pyNIBS 0.2024.8__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pyNIBS-0.2024.8.dist-info/LICENSE +623 -0
- pyNIBS-0.2024.8.dist-info/METADATA +723 -0
- pyNIBS-0.2024.8.dist-info/RECORD +107 -0
- pyNIBS-0.2024.8.dist-info/WHEEL +5 -0
- pyNIBS-0.2024.8.dist-info/top_level.txt +1 -0
- pynibs/__init__.py +34 -0
- pynibs/coil.py +1367 -0
- pynibs/congruence/__init__.py +15 -0
- pynibs/congruence/congruence.py +1108 -0
- pynibs/congruence/ext_metrics.py +257 -0
- pynibs/congruence/stimulation_threshold.py +318 -0
- pynibs/data/configuration_exp0.yaml +59 -0
- pynibs/data/configuration_linear_MEP.yaml +61 -0
- pynibs/data/configuration_linear_RT.yaml +61 -0
- pynibs/data/configuration_sigmoid4.yaml +68 -0
- pynibs/data/network mapping configuration/configuration guide.md +238 -0
- pynibs/data/network mapping configuration/configuration_TEMPLATE.yaml +42 -0
- pynibs/data/network mapping configuration/configuration_for_testing.yaml +43 -0
- pynibs/data/network mapping configuration/configuration_modelTMS.yaml +43 -0
- pynibs/data/network mapping configuration/configuration_reg_isi_05.yaml +43 -0
- pynibs/data/network mapping configuration/output_documentation.md +185 -0
- pynibs/data/network mapping configuration/recommendations_for_accuracy_threshold.md +77 -0
- pynibs/data/neuron/models/L23_PC_cADpyr_biphasic_v1.csv +1281 -0
- pynibs/data/neuron/models/L23_PC_cADpyr_monophasic_v1.csv +1281 -0
- pynibs/data/neuron/models/L4_LBC_biphasic_v1.csv +1281 -0
- pynibs/data/neuron/models/L4_LBC_monophasic_v1.csv +1281 -0
- pynibs/data/neuron/models/L4_NBC_biphasic_v1.csv +1281 -0
- pynibs/data/neuron/models/L4_NBC_monophasic_v1.csv +1281 -0
- pynibs/data/neuron/models/L4_SBC_biphasic_v1.csv +1281 -0
- pynibs/data/neuron/models/L4_SBC_monophasic_v1.csv +1281 -0
- pynibs/data/neuron/models/L5_TTPC2_cADpyr_biphasic_v1.csv +1281 -0
- pynibs/data/neuron/models/L5_TTPC2_cADpyr_monophasic_v1.csv +1281 -0
- pynibs/expio/Mep.py +1518 -0
- pynibs/expio/__init__.py +8 -0
- pynibs/expio/brainsight.py +979 -0
- pynibs/expio/brainvis.py +71 -0
- pynibs/expio/cobot.py +239 -0
- pynibs/expio/exp.py +1876 -0
- pynibs/expio/fit_funs.py +287 -0
- pynibs/expio/localite.py +1987 -0
- pynibs/expio/signal_ced.py +51 -0
- pynibs/expio/visor.py +624 -0
- pynibs/freesurfer.py +502 -0
- pynibs/hdf5_io/__init__.py +10 -0
- pynibs/hdf5_io/hdf5_io.py +1857 -0
- pynibs/hdf5_io/xdmf.py +1542 -0
- pynibs/mesh/__init__.py +3 -0
- pynibs/mesh/mesh_struct.py +1394 -0
- pynibs/mesh/transformations.py +866 -0
- pynibs/mesh/utils.py +1103 -0
- pynibs/models/_TMS.py +211 -0
- pynibs/models/__init__.py +0 -0
- pynibs/muap.py +392 -0
- pynibs/neuron/__init__.py +2 -0
- pynibs/neuron/neuron_regression.py +284 -0
- pynibs/neuron/util.py +58 -0
- pynibs/optimization/__init__.py +5 -0
- pynibs/optimization/multichannel.py +278 -0
- pynibs/optimization/opt_mep.py +152 -0
- pynibs/optimization/optimization.py +1445 -0
- pynibs/optimization/workhorses.py +698 -0
- pynibs/pckg/__init__.py +0 -0
- pynibs/pckg/biosig/biosig4c++-1.9.5.src_fixed.tar.gz +0 -0
- pynibs/pckg/libeep/__init__.py +0 -0
- pynibs/pckg/libeep/pyeep.so +0 -0
- pynibs/regression/__init__.py +11 -0
- pynibs/regression/dual_node_detection.py +2375 -0
- pynibs/regression/regression.py +2984 -0
- pynibs/regression/score_types.py +0 -0
- pynibs/roi/__init__.py +2 -0
- pynibs/roi/roi.py +895 -0
- pynibs/roi/roi_structs.py +1233 -0
- pynibs/subject.py +1009 -0
- pynibs/tensor_scaling.py +144 -0
- pynibs/tests/data/InstrumentMarker20200225163611937.xml +19 -0
- pynibs/tests/data/TriggerMarkers_Coil0_20200225163443682.xml +14 -0
- pynibs/tests/data/TriggerMarkers_Coil1_20200225170337572.xml +6373 -0
- pynibs/tests/data/Xdmf.dtd +89 -0
- pynibs/tests/data/brainsight_niiImage_nifticoord.txt +145 -0
- pynibs/tests/data/brainsight_niiImage_nifticoord_largefile.txt +1434 -0
- pynibs/tests/data/brainsight_niiImage_niifticoord_mixedtargets.txt +47 -0
- pynibs/tests/data/create_subject_testsub.py +332 -0
- pynibs/tests/data/data.hdf5 +0 -0
- pynibs/tests/data/geo.hdf5 +0 -0
- pynibs/tests/test_coil.py +474 -0
- pynibs/tests/test_elements2nodes.py +100 -0
- pynibs/tests/test_hdf5_io/test_xdmf.py +61 -0
- pynibs/tests/test_mesh_transformations.py +123 -0
- pynibs/tests/test_mesh_utils.py +143 -0
- pynibs/tests/test_nnav_imports.py +101 -0
- pynibs/tests/test_quality_measures.py +117 -0
- pynibs/tests/test_regressdata.py +289 -0
- pynibs/tests/test_roi.py +17 -0
- pynibs/tests/test_rotations.py +86 -0
- pynibs/tests/test_subject.py +71 -0
- pynibs/tests/test_util.py +24 -0
- pynibs/tms_pulse.py +34 -0
- pynibs/util/__init__.py +4 -0
- pynibs/util/dosing.py +233 -0
- pynibs/util/quality_measures.py +562 -0
- pynibs/util/rotations.py +340 -0
- pynibs/util/simnibs.py +763 -0
- pynibs/util/util.py +727 -0
- pynibs/visualization/__init__.py +2 -0
- pynibs/visualization/para.py +4372 -0
- pynibs/visualization/plot_2D.py +137 -0
- pynibs/visualization/render_3D.py +347 -0
pynibs/coil.py
ADDED
|
@@ -0,0 +1,1367 @@
|
|
|
1
|
+
"""
|
|
2
|
+
All functions to operate on TMS coils go here, for example to create ``.xdmf`` files to visualize coil positions.
|
|
3
|
+
"""
|
|
4
|
+
import os
|
|
5
|
+
import copy
|
|
6
|
+
import math
|
|
7
|
+
import h5py
|
|
8
|
+
import shutil
|
|
9
|
+
import random
|
|
10
|
+
import itertools
|
|
11
|
+
import pandas as pd
|
|
12
|
+
import numpy as np
|
|
13
|
+
import matplotlib.pyplot as plt
|
|
14
|
+
from scipy.spatial import Delaunay
|
|
15
|
+
from collections import OrderedDict
|
|
16
|
+
|
|
17
|
+
import pynibs
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def get_coil_dipole_pos(coil_fn, matsimnibs):
|
|
21
|
+
"""
|
|
22
|
+
Apply transformation to coil dipoles and return position.
|
|
23
|
+
|
|
24
|
+
Parameters
|
|
25
|
+
----------
|
|
26
|
+
coil_fn : str
|
|
27
|
+
Filename of coil .ccd file.
|
|
28
|
+
matsimnibs : np.ndarray of float
|
|
29
|
+
Transformation matrix.
|
|
30
|
+
|
|
31
|
+
Returns
|
|
32
|
+
-------
|
|
33
|
+
dipoles_pos : np.ndarray
|
|
34
|
+
(N, 3) Cartesian coordinates (x, y, z) of coil magnetic dipoles.
|
|
35
|
+
"""
|
|
36
|
+
if coil_fn[-3:] == "nii":
|
|
37
|
+
coil_fn = coil_fn[:-3] + "ccd"
|
|
38
|
+
coil_data = np.genfromtxt(coil_fn, delimiter=' ', skip_header=3)
|
|
39
|
+
coil_dipoles = coil_data[:, 0:3]
|
|
40
|
+
coil_dipoles *= 1e3
|
|
41
|
+
coil_dipoles = np.hstack([coil_dipoles, np.ones((coil_dipoles.shape[0], 1))])
|
|
42
|
+
|
|
43
|
+
return matsimnibs.dot(coil_dipoles.T).T[:, :3]
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def check_coil_position(points, hull):
|
|
47
|
+
"""
|
|
48
|
+
Check if magnetic dipoles are lying inside head region
|
|
49
|
+
|
|
50
|
+
Parameters
|
|
51
|
+
----------
|
|
52
|
+
points : np.ndarray of float
|
|
53
|
+
(N_points, 3) Coordinates (x,y,z) of magnetic dipoles
|
|
54
|
+
hull : Delaunay object or np.ndarray of float
|
|
55
|
+
(N_surface_points, 3) Head surface data
|
|
56
|
+
|
|
57
|
+
Returns
|
|
58
|
+
-------
|
|
59
|
+
valid : bool
|
|
60
|
+
Validity of coil position:
|
|
61
|
+
TRUE: valid
|
|
62
|
+
FALSE: unvalid
|
|
63
|
+
"""
|
|
64
|
+
# make Delaunay grid if not already passed
|
|
65
|
+
if not isinstance(hull, Delaunay):
|
|
66
|
+
hull = Delaunay(hull)
|
|
67
|
+
|
|
68
|
+
# filter out points which are outside bounding box to save some time
|
|
69
|
+
bounds = [np.min(hull.points), np.max(hull.points)]
|
|
70
|
+
points_inside = np.logical_and((points > bounds[0]).all(axis=1),
|
|
71
|
+
(points < bounds[1]).all(axis=1))
|
|
72
|
+
|
|
73
|
+
# test if points are inside (True)
|
|
74
|
+
inside = hull.find_simplex(points[points_inside]) >= 0
|
|
75
|
+
valid = not (inside.any())
|
|
76
|
+
|
|
77
|
+
return valid
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def calc_coil_transformation_matrix(LOC_mean, ORI_mean, LOC_var, ORI_var, V):
|
|
81
|
+
"""
|
|
82
|
+
Calculate the modified coil transformation matrix needed for SimNIBS based on location and orientation
|
|
83
|
+
variations observed in the framework of uncertainty analysis
|
|
84
|
+
|
|
85
|
+
Parameters
|
|
86
|
+
----------
|
|
87
|
+
LOC_mean : np.ndarray of float
|
|
88
|
+
(3), Mean location of TMS coil
|
|
89
|
+
ORI_mean : np.ndarray of float
|
|
90
|
+
(3 x 3) Mean orientations of TMS coil
|
|
91
|
+
|
|
92
|
+
.. math::
|
|
93
|
+
\\begin{bmatrix}
|
|
94
|
+
| & | & | \\\\
|
|
95
|
+
x & y & z \\\\
|
|
96
|
+
| & | & | \\\\
|
|
97
|
+
\\end{bmatrix}
|
|
98
|
+
|
|
99
|
+
LOC_var : np.ndarray of float
|
|
100
|
+
(3) Location variation in normalized space (dx', dy', dz'), i.e. zero mean and projected on principal axes
|
|
101
|
+
ORI_var : np.ndarray of float
|
|
102
|
+
(3) Orientation variation expressed in Euler angles [alpha, beta, gamma] in deg
|
|
103
|
+
V : np.ndarray of float
|
|
104
|
+
(3x3) V-matrix containing the eigenvectors from _,_,V = numpy.linalg.svd
|
|
105
|
+
|
|
106
|
+
Returns
|
|
107
|
+
-------
|
|
108
|
+
mat : np.ndarray of float
|
|
109
|
+
(4, 4) Transformation matrix containing 3 axis and 1 location vector:
|
|
110
|
+
|
|
111
|
+
.. math::
|
|
112
|
+
\\begin{bmatrix}
|
|
113
|
+
| & | & | & | \\\\
|
|
114
|
+
x & y & z & pos \\\\
|
|
115
|
+
| & | & | & | \\\\
|
|
116
|
+
0 & 0 & 0 & 1 \\\\
|
|
117
|
+
\\end{bmatrix}
|
|
118
|
+
"""
|
|
119
|
+
# calculate rotation matrix for angle variation (angle_var in deg)
|
|
120
|
+
rotation_matrix = pynibs.euler_angles_to_rotation_matrix(ORI_var * np.pi / 180.)
|
|
121
|
+
|
|
122
|
+
# determine new orientation
|
|
123
|
+
ori = np.dot(ORI_mean, rotation_matrix)
|
|
124
|
+
|
|
125
|
+
# determine new location by retransforming from normalized space to regular space
|
|
126
|
+
locations = np.dot(LOC_var, V) + LOC_mean
|
|
127
|
+
|
|
128
|
+
# concatenate results
|
|
129
|
+
mat = np.hstack((ori, locations[:, np.newaxis]))
|
|
130
|
+
mat = np.vstack((mat, [0, 0, 0, 1]))
|
|
131
|
+
|
|
132
|
+
return mat
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def calc_coil_position_pdf(fn_rescon=None, fn_simpos=None, fn_exp=None, orientation='quaternions',
|
|
136
|
+
folder_pdfplots=None):
|
|
137
|
+
"""
|
|
138
|
+
Determines the probability density functions of the transformed coil position (x', y', z') and quaternions of
|
|
139
|
+
the coil orientations (x'', y'', z'')
|
|
140
|
+
|
|
141
|
+
Parameters
|
|
142
|
+
----------
|
|
143
|
+
fn_rescon : str
|
|
144
|
+
Filename of the results file from TMS experiments (results_conditions.csv)
|
|
145
|
+
fn_simpos : str
|
|
146
|
+
Filename of the positions and orientation from TMS experiments (simPos.csv)
|
|
147
|
+
fn_exp : str
|
|
148
|
+
Filename of experimental.csv file from experiments
|
|
149
|
+
orientation: str
|
|
150
|
+
Type of orientation estimation: 'quaternions' or 'euler'
|
|
151
|
+
folder_pdfplots : str
|
|
152
|
+
Folder, where the plots of the fitted pdfs are saved (omitted if not provided)
|
|
153
|
+
|
|
154
|
+
Returns
|
|
155
|
+
-------
|
|
156
|
+
pdf_paras_location : list of list of np.ndarray
|
|
157
|
+
[n_conditions] Pdf parameters (limits and shape) of the coil position for x', y', and z' for each:
|
|
158
|
+
|
|
159
|
+
- beta_paras ... [p, q, a, b] (2 shape parameters and limits)
|
|
160
|
+
- moments ... [data_mean, data_std, beta_mean, beta_std]
|
|
161
|
+
- p_value ... p-value of the Kolmogorov Smirnov test
|
|
162
|
+
- uni_paras ... [a, b] (limits)
|
|
163
|
+
pdf_paras_orientation_euler : list of np.ndarray
|
|
164
|
+
[n_conditions] Pdf parameters (limits and shape) of the coil orientation Psi, Theta, and Phi for each:
|
|
165
|
+
|
|
166
|
+
- beta_paras ... [p, q, a, b] (2 shape parameters and limits)
|
|
167
|
+
- moments ... [data_mean, data_std, beta_mean, beta_std]
|
|
168
|
+
- p_value ... p-value of the Kolmogorov Smirnov test
|
|
169
|
+
- uni_paras ... [a, b] (limits)
|
|
170
|
+
OP_mean : List of [3 x 4] np.ndarray
|
|
171
|
+
[n_conditions] List of mean coil position and orientation for different conditions (global coordinate system)
|
|
172
|
+
|
|
173
|
+
.. math::
|
|
174
|
+
\\begin{bmatrix}
|
|
175
|
+
| & | & | & | \\\\
|
|
176
|
+
ori_x & ori_y & ori_z & pos \\\\
|
|
177
|
+
| & | & | & | \\\\
|
|
178
|
+
\\end{bmatrix}
|
|
179
|
+
|
|
180
|
+
OP_zeromean : list of [3 x 4 x n_con_each] np.ndarray [n_conditions]
|
|
181
|
+
List over conditions containing zero-mean coil orientations and positions
|
|
182
|
+
V : list of [3 x 3] np.ndarrays [n_conditions]
|
|
183
|
+
Transformation matrix of coil positions from global coordinate system to transformed coordinate system
|
|
184
|
+
P_transform : list of np.ndarray [n_conditions]
|
|
185
|
+
List over conditions containing transformed coil positions [x', y', z'] of all stimulations
|
|
186
|
+
(zero-mean, rotated by SVD)
|
|
187
|
+
quaternions : list of np.ndarray [n_conditions]
|
|
188
|
+
List over conditions containing imaginary part of quaternions [x'', y'', z''] of all stimulations
|
|
189
|
+
"""
|
|
190
|
+
import pygpc
|
|
191
|
+
|
|
192
|
+
if folder_pdfplots is not None:
|
|
193
|
+
make_pdf_plot = True
|
|
194
|
+
else:
|
|
195
|
+
make_pdf_plot = False
|
|
196
|
+
folder_pdfplots = ''
|
|
197
|
+
|
|
198
|
+
if fn_rescon and fn_simpos:
|
|
199
|
+
positions_all, conditions, position_list, _, _ = pynibs.read_exp_stimulations(fn_rescon, fn_simpos)
|
|
200
|
+
|
|
201
|
+
# sort POSITIONS according to CONDITIONS, idx_con is alphabetically sorted, first index of condition[*]
|
|
202
|
+
# appeareance
|
|
203
|
+
conditions_unique, idx_con, n_con_each = np.unique(conditions,
|
|
204
|
+
return_index=True,
|
|
205
|
+
return_inverse=False,
|
|
206
|
+
return_counts=True)
|
|
207
|
+
|
|
208
|
+
idx_con_sort = np.argsort(idx_con)
|
|
209
|
+
conditions_unique = conditions_unique[idx_con_sort] # conditions_unique is ordered like experimental data
|
|
210
|
+
n_con_each = n_con_each[idx_con_sort]
|
|
211
|
+
n_condition = len(conditions_unique)
|
|
212
|
+
n_zaps = len(positions_all)
|
|
213
|
+
|
|
214
|
+
pos_idx = 0
|
|
215
|
+
k = 0
|
|
216
|
+
|
|
217
|
+
# Orientation and position tensors (list over conditions containing arrays of [3 x 4 x n_con_each])
|
|
218
|
+
op = [np.zeros((3, 4, n_con_each[i])) for i in range(n_condition)]
|
|
219
|
+
|
|
220
|
+
# read data from global position_list
|
|
221
|
+
for i in range(n_condition):
|
|
222
|
+
while position_list[pos_idx][len(position_list[0]) - 3] == conditions_unique[i]:
|
|
223
|
+
op[i][:, :, k] = np.array(positions_all[pos_idx])[0:3, 0:4]
|
|
224
|
+
pos_idx = pos_idx + 1
|
|
225
|
+
k = k + 1
|
|
226
|
+
if pos_idx == n_zaps:
|
|
227
|
+
break
|
|
228
|
+
k = 0
|
|
229
|
+
|
|
230
|
+
# determine matrix of mean location and orientation (list over conditions containing arrays of [3 x 4])
|
|
231
|
+
op_mean = [np.mean(op[i], axis=2) for i in range(n_condition)]
|
|
232
|
+
|
|
233
|
+
elif fn_exp:
|
|
234
|
+
exp = pynibs.read_csv(fn_exp)
|
|
235
|
+
exp_cond = pynibs.sort_by_condition(exp)
|
|
236
|
+
|
|
237
|
+
conditions_unique = [exp_cond[i_cond]['condition'][0] for i_cond in range(len(exp_cond))]
|
|
238
|
+
|
|
239
|
+
n_con_each = [len(e['condition']) for e in exp_cond]
|
|
240
|
+
n_condition = len(exp_cond)
|
|
241
|
+
|
|
242
|
+
# Orientation and position tensors (list over conditions containing arrays of [3 x 4 x n_con_each])
|
|
243
|
+
op = [np.zeros((3, 4, n_con_each[i])) for i in range(n_condition)]
|
|
244
|
+
|
|
245
|
+
for i in range(n_condition):
|
|
246
|
+
for k in range(n_con_each[i]):
|
|
247
|
+
op[i][:, :, k] = exp_cond[i]['coil_mean_matrix'][k][0:3, :]
|
|
248
|
+
|
|
249
|
+
# determine matrix of mean location and orientation (list over conditions containing arrays of [3 x 4])
|
|
250
|
+
op_mean = [np.mean(op[i], axis=2) for i in range(n_condition)]
|
|
251
|
+
|
|
252
|
+
else:
|
|
253
|
+
raise AssertionError('Please provide experiment.csv or results_condition.csv and simpos.csv files!')
|
|
254
|
+
|
|
255
|
+
# Initialize arrays
|
|
256
|
+
# cond_0, zap_1 zap_2
|
|
257
|
+
# [ | | | | ] [ | | | | ]
|
|
258
|
+
# [ ori_x ori_y ori_z pos ] . . . [ ori_x ori_y ori_z pos ] . . .
|
|
259
|
+
# [ | | | | ] [ | | | | ]
|
|
260
|
+
|
|
261
|
+
# Orientation and position tensors with zeromean (list over conditions containing arrays of [3 x 4 x n_con_each])
|
|
262
|
+
op_zeromean = [np.zeros((3, 4, n_con_each[i])) for i in range(n_condition)]
|
|
263
|
+
|
|
264
|
+
# Transformed coil position in (x', y', z') space (list over conditions containing arrays of [n_con_each x 3])
|
|
265
|
+
p_transform = [0] * n_condition
|
|
266
|
+
|
|
267
|
+
# SVD matrices
|
|
268
|
+
u = [0] * n_condition
|
|
269
|
+
s = [0] * n_condition
|
|
270
|
+
v = [0] * n_condition
|
|
271
|
+
|
|
272
|
+
# pdf parameters
|
|
273
|
+
pdf_paras_location = [[[] for _ in range(3)] for _ in range(n_condition)]
|
|
274
|
+
pdf_paras_orientation_euler = [[[] for _ in range(3)] for _ in range(n_condition)]
|
|
275
|
+
pos_names = ['xp', 'yp', 'zp']
|
|
276
|
+
orientations = [0] * n_condition
|
|
277
|
+
|
|
278
|
+
for i in range(n_condition):
|
|
279
|
+
|
|
280
|
+
# shift data by mean
|
|
281
|
+
op_zeromean[i][:, 3, :] = op[i][:, 3, :] - np.tile(op_mean[i][:, 3][:, np.newaxis], (1, n_con_each[i]))
|
|
282
|
+
# OP[i][0:3,3,:] - np.tile(OP_mean[i][:, 3][:, np.newaxis], (1, n_con_each[i]))
|
|
283
|
+
|
|
284
|
+
# perform SVD to determine principal axis
|
|
285
|
+
u[i], s[i], v[i] = np.linalg.svd(op_zeromean[i][:, 3, :].T, full_matrices=True)
|
|
286
|
+
|
|
287
|
+
# project locations on principal axis
|
|
288
|
+
p_transform[i] = np.dot(op_zeromean[i][:, 3, :].T, v[i].T)
|
|
289
|
+
|
|
290
|
+
# rotate coil orientations to mean orientation (zero-mean)
|
|
291
|
+
op_zeromean[i][:, 0:3, :] = np.dot(op[i][:, 0:3, :].transpose(2, 1, 0), op_mean[i][:, 0:3]).transpose(2, 1, 0)
|
|
292
|
+
|
|
293
|
+
# Determine quaternions or Euler angles
|
|
294
|
+
orientations[i] = np.zeros((n_con_each[i], 3))
|
|
295
|
+
xlabel, ylabel = '', ''
|
|
296
|
+
|
|
297
|
+
for j in range(n_con_each[i]):
|
|
298
|
+
if orientation == 'quaternions':
|
|
299
|
+
orientations[i][j, :] = pynibs.rot_to_quat(op_zeromean[i][:, 0:3, j])[1:]
|
|
300
|
+
ori_names = ['xq', 'yq', 'zq']
|
|
301
|
+
xlabel = ['$x_q$', '$y_q$', '$z_q$']
|
|
302
|
+
ylabel = ['$p(x_q)$', '$p(y_q)$', '$p(z_q)$']
|
|
303
|
+
|
|
304
|
+
elif orientation == 'euler':
|
|
305
|
+
orientations[i][j, :] = pynibs.rotation_matrix_to_euler_angles(op_zeromean[i][:, :, j]) * 180.0 / np.pi
|
|
306
|
+
ori_names = ['Psi', 'Theta', 'Phi']
|
|
307
|
+
xlabel = [r'$\Psi$', r'$\Theta$', r'$\Phi$']
|
|
308
|
+
ylabel = [r'$p(\Psi)$', r'$p(\Theta)$', r'$p(\Phi)$']
|
|
309
|
+
|
|
310
|
+
else:
|
|
311
|
+
raise ValueError('Please set orientation parameter to either "quaternions" or "euler"')
|
|
312
|
+
|
|
313
|
+
# fit beta pdfs to x', y' and z' locations and orientations
|
|
314
|
+
for j, name in enumerate(pos_names):
|
|
315
|
+
pdf_paras_location[i][j] = pygpc.fit_betapdf(p_transform[i][:, j],
|
|
316
|
+
BETATOL=0.05,
|
|
317
|
+
PUNI=0.9,
|
|
318
|
+
PLOT=make_pdf_plot,
|
|
319
|
+
VISI=0,
|
|
320
|
+
xlabel=xlabel[j],
|
|
321
|
+
ylabel=ylabel[j],
|
|
322
|
+
filename=os.path.join(folder_pdfplots,
|
|
323
|
+
'POS_' +
|
|
324
|
+
conditions_unique[i] + '_' +
|
|
325
|
+
name +
|
|
326
|
+
'_betafit'))
|
|
327
|
+
|
|
328
|
+
for j, name in enumerate(ori_names):
|
|
329
|
+
pdf_paras_orientation_euler[i][j] = pygpc.fit_betapdf(orientations[i][:, j],
|
|
330
|
+
BETATOL=0.05,
|
|
331
|
+
PUNI=0.9,
|
|
332
|
+
PLOT=make_pdf_plot,
|
|
333
|
+
VISI=0,
|
|
334
|
+
xlabel=xlabel[j],
|
|
335
|
+
ylabel=ylabel[j],
|
|
336
|
+
filename=os.path.join(folder_pdfplots,
|
|
337
|
+
'ORI_' +
|
|
338
|
+
conditions_unique[i] + '_' +
|
|
339
|
+
name +
|
|
340
|
+
'_betafit'))
|
|
341
|
+
|
|
342
|
+
return pdf_paras_location, pdf_paras_orientation_euler, op_mean, op_zeromean, v, p_transform, orientations
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
def test_coil_position_gpc(parameters):
|
|
346
|
+
"""
|
|
347
|
+
Testing valid coil positions for gPC analysis
|
|
348
|
+
|
|
349
|
+
Parameters
|
|
350
|
+
----------
|
|
351
|
+
|
|
352
|
+
Returns
|
|
353
|
+
-------
|
|
354
|
+
|
|
355
|
+
"""
|
|
356
|
+
from pygpc.RandomParameter import RandomParameter
|
|
357
|
+
|
|
358
|
+
# load subject
|
|
359
|
+
subject = pynibs.load_subject(parameters["fn_subject"])
|
|
360
|
+
|
|
361
|
+
coil_uncertainty = False
|
|
362
|
+
parameters_random = OrderedDict()
|
|
363
|
+
|
|
364
|
+
# extract random parameters
|
|
365
|
+
for p in parameters:
|
|
366
|
+
if isinstance(parameters[p], RandomParameter):
|
|
367
|
+
parameters_random[p] = parameters[p]
|
|
368
|
+
if p in ["x", "y", "z", "psi", "theta", "phi"]:
|
|
369
|
+
coil_uncertainty = True
|
|
370
|
+
|
|
371
|
+
if coil_uncertainty:
|
|
372
|
+
|
|
373
|
+
svd_v = parameters["coil_position_mean"][0:3, 0:3]
|
|
374
|
+
|
|
375
|
+
# Loading geometry to create Delaunay object of outer skin surface
|
|
376
|
+
with h5py.File(subject.mesh[parameters["mesh_idx"]]['fn_mesh_hdf5'], 'r') as f:
|
|
377
|
+
points = np.array(f['mesh/nodes/node_coord'])
|
|
378
|
+
node_number_list = np.array(f['mesh/elm/node_number_list'])
|
|
379
|
+
elm_type = np.array(f['mesh/elm/elm_type'])
|
|
380
|
+
regions = np.array(f['mesh/elm/tag1'])
|
|
381
|
+
triangles_regions = regions[elm_type == 2,] - 1000
|
|
382
|
+
triangles = node_number_list[elm_type == 2, 0:3]
|
|
383
|
+
|
|
384
|
+
triangles = triangles[triangles_regions == 5]
|
|
385
|
+
surface_points = pynibs.unique_rows(np.reshape(points[triangles], (3 * triangles.shape[0], 3)))
|
|
386
|
+
limits_scaling_factor = .1
|
|
387
|
+
|
|
388
|
+
# Generate Delaunay triangulation object
|
|
389
|
+
dobj = Delaunay(surface_points)
|
|
390
|
+
|
|
391
|
+
del points, node_number_list, elm_type, regions, triangles_regions, triangles, surface_points
|
|
392
|
+
|
|
393
|
+
# initialize parameter dictionary
|
|
394
|
+
parameters_corr = dict()
|
|
395
|
+
parameters_corr_idx = dict()
|
|
396
|
+
coil_param = ['x', 'y', 'z', 'psi', 'theta', 'phi']
|
|
397
|
+
|
|
398
|
+
for i, c in enumerate(coil_param):
|
|
399
|
+
parameters_corr[c] = {'pdf_limits': [0, 0], 'pdf_shape': [0, 0]}
|
|
400
|
+
parameters_corr_idx[i] = c
|
|
401
|
+
|
|
402
|
+
# we need to map some indices
|
|
403
|
+
dic_par_name = dict.fromkeys([0, 1, 2], "pdf_paras_location")
|
|
404
|
+
dic_par_name.update(dict.fromkeys([3, 4, 5], "pdf_paras_orientation_euler"))
|
|
405
|
+
dic_limit_id = {0: 0, 2: 1} # translates from get_invalid_coil_parameters limits id to pdf_paras_* id
|
|
406
|
+
dic_param_id = dict.fromkeys([0, 3], 0)
|
|
407
|
+
dic_param_id.update(dict.fromkeys([1, 4], 1))
|
|
408
|
+
dic_param_id.update(dict.fromkeys([2, 5], 2))
|
|
409
|
+
|
|
410
|
+
# Determine bad parameters and correct them
|
|
411
|
+
print(("Checking validity of coil position uncertainty for condition " + parameters["cond"]))
|
|
412
|
+
random.seed(1)
|
|
413
|
+
|
|
414
|
+
# gather random variables in parameter dict (constants = 0 = no variation)
|
|
415
|
+
for i_rv, rv in enumerate(parameters_random):
|
|
416
|
+
if rv in coil_param:
|
|
417
|
+
parameters_corr[rv] = {'pdf_limits': parameters_random[rv].pdf_limits,
|
|
418
|
+
'pdf_shape': parameters_random[rv].pdf_shape}
|
|
419
|
+
|
|
420
|
+
bad_params = get_invalid_coil_parameters(parameters_corr,
|
|
421
|
+
coil_position_mean=parameters["coil_position_mean"],
|
|
422
|
+
svd_v=svd_v,
|
|
423
|
+
del_obj=dobj,
|
|
424
|
+
fn_coil=parameters["fn_coil"])
|
|
425
|
+
|
|
426
|
+
# change param limits until no dipoles left inside head
|
|
427
|
+
while bad_params:
|
|
428
|
+
print(" > Invalid parameter found!")
|
|
429
|
+
# several parameters may lead to a bad position
|
|
430
|
+
# take a random parameter from these
|
|
431
|
+
param_id = random.sample(list(range(len(bad_params))), 1)[0]
|
|
432
|
+
|
|
433
|
+
# change last bad_params[-1]' limit
|
|
434
|
+
param_to_change_idx = bad_params[param_id][0]
|
|
435
|
+
limit_to_change_idx = dic_limit_id[bad_params[param_id][1]] # change min or max limit from 3 to 2 limit
|
|
436
|
+
|
|
437
|
+
# grab old limits of parameter to change
|
|
438
|
+
limits_old = parameters_corr[parameters_corr_idx[param_to_change_idx]]['pdf_limits']
|
|
439
|
+
|
|
440
|
+
# rescale limits (but change only important boundary, min or max)
|
|
441
|
+
factor = (limits_old[1] - limits_old[0]) * limits_scaling_factor
|
|
442
|
+
limits_temp = [limits_old[0] + factor, limits_old[1] - factor]
|
|
443
|
+
limits_new = copy.deepcopy(limits_old)
|
|
444
|
+
limits_new[limit_to_change_idx] = limits_temp[limit_to_change_idx]
|
|
445
|
+
parameters_corr[parameters_corr_idx[param_to_change_idx]]['pdf_limits'] = limits_new
|
|
446
|
+
|
|
447
|
+
print((' > Changing pdf_limits of {} from {} -> {}'.format(parameters_corr_idx[param_to_change_idx],
|
|
448
|
+
limits_old,
|
|
449
|
+
limits_new)))
|
|
450
|
+
|
|
451
|
+
# overwrite old values
|
|
452
|
+
parameters[parameters_corr_idx[param_to_change_idx]].pdf_limits = limits_new
|
|
453
|
+
|
|
454
|
+
# repeat dipole check
|
|
455
|
+
bad_params = get_invalid_coil_parameters(parameters_corr,
|
|
456
|
+
coil_position_mean=parameters["coil_position_mean"],
|
|
457
|
+
svd_v=svd_v,
|
|
458
|
+
del_obj=dobj,
|
|
459
|
+
fn_coil=parameters["fn_coil"])
|
|
460
|
+
|
|
461
|
+
return parameters
|
|
462
|
+
|
|
463
|
+
|
|
464
|
+
def get_invalid_coil_parameters(param_dict, coil_position_mean, svd_v, del_obj, fn_coil,
|
|
465
|
+
fn_hdf5_coilpos=None):
|
|
466
|
+
"""
|
|
467
|
+
Finds gpc parameter combinations, which place coil dipoles inside subjects head.
|
|
468
|
+
Only endpoints (and midpoints) of the parameter ranges are examined.
|
|
469
|
+
|
|
470
|
+
get_invalid_coil_parameters(param_dict, pos_mean, v, del_obj, fn_coil, fn_hdf5_coilpos=None)
|
|
471
|
+
|
|
472
|
+
Parameters
|
|
473
|
+
----------
|
|
474
|
+
param_dict : dict
|
|
475
|
+
Dictionary containing dictionary with ``'limits'`` and ``'pdfshape'``.
|
|
476
|
+
keys: ``'x'``, ``'y'``, ``'z'``, ``'psi'``, ``'theta'``, ``'phi'``.
|
|
477
|
+
coil_position_mean: np.ndarray
|
|
478
|
+
(3, 4) Mean coil positions and orientations.
|
|
479
|
+
svd_v : np.ndarray
|
|
480
|
+
(3, 3) SVD matrix V.
|
|
481
|
+
del_obj : :class:`scipy.spatial.Delaunay`
|
|
482
|
+
Skin surface.
|
|
483
|
+
fn_coil : str
|
|
484
|
+
Filename of coil .ccd file.
|
|
485
|
+
fn_hdf5_coilpos : str
|
|
486
|
+
Filename of .hdf5 file to save coil_pos in (incl. path and .hdf5 extension).
|
|
487
|
+
|
|
488
|
+
Returns
|
|
489
|
+
-------
|
|
490
|
+
fail_params: list of int
|
|
491
|
+
Index and combination of failed parameter.
|
|
492
|
+
"""
|
|
493
|
+
# for every condition:
|
|
494
|
+
results = []
|
|
495
|
+
|
|
496
|
+
# for i in range(6):
|
|
497
|
+
limits_pos_x = param_dict['x']['limits']
|
|
498
|
+
limits_pos_y = param_dict['y']['limits']
|
|
499
|
+
limits_pos_z = param_dict['z']['limits']
|
|
500
|
+
limits_psi = param_dict['psi']['limits']
|
|
501
|
+
limits_theta = param_dict['theta']['limits']
|
|
502
|
+
limits_phi = param_dict['phi']['limits']
|
|
503
|
+
|
|
504
|
+
limits_pos_x = pynibs.add_center(limits_pos_x)
|
|
505
|
+
limits_pos_y = pynibs.add_center(limits_pos_y)
|
|
506
|
+
limits_pos_z = pynibs.add_center(limits_pos_z)
|
|
507
|
+
limits_psi = pynibs.add_center(limits_psi)
|
|
508
|
+
limits_theta = pynibs.add_center(limits_theta)
|
|
509
|
+
limits_phi = pynibs.add_center(limits_phi)
|
|
510
|
+
|
|
511
|
+
temp_list = [[0, 1, 2], [0, 1, 2], [0, 1, 2], [0, 1, 2], [0, 1, 2], [0, 1, 2]]
|
|
512
|
+
combinations = list(itertools.product(*temp_list))
|
|
513
|
+
del temp_list
|
|
514
|
+
|
|
515
|
+
for combination in combinations:
|
|
516
|
+
|
|
517
|
+
# create matsimnibs
|
|
518
|
+
loc_var = np.array([limits_pos_x[combination[0]],
|
|
519
|
+
limits_pos_y[combination[1]],
|
|
520
|
+
limits_pos_z[combination[2]]])
|
|
521
|
+
ori_var = np.array([limits_psi[combination[3]],
|
|
522
|
+
limits_theta[combination[4]],
|
|
523
|
+
limits_phi[combination[5]]])
|
|
524
|
+
|
|
525
|
+
mat = calc_coil_transformation_matrix(LOC_mean=coil_position_mean[0:3, 3],
|
|
526
|
+
ORI_mean=coil_position_mean[0:3, 0:3],
|
|
527
|
+
LOC_var=loc_var,
|
|
528
|
+
ORI_var=ori_var,
|
|
529
|
+
V=svd_v)
|
|
530
|
+
|
|
531
|
+
# get dipole points for coil for actual matsimnibs
|
|
532
|
+
coil_dipoles = get_coil_dipole_pos(fn_coil, mat)
|
|
533
|
+
|
|
534
|
+
if fn_hdf5_coilpos:
|
|
535
|
+
with h5py.File(fn_hdf5_coilpos, 'w') as f:
|
|
536
|
+
f.create_dataset("/dipoles/", data=coil_dipoles)
|
|
537
|
+
|
|
538
|
+
with open(os.path.splitext(fn_hdf5_coilpos)[0] + ".xdmf", 'w') as f:
|
|
539
|
+
f.write('<?xml version="1.0"?>\n')
|
|
540
|
+
f.write('<!DOCTYPE Xdmf SYSTEM "Xdmf.dtd" []>\n')
|
|
541
|
+
f.write('<Xdmf Version="2.0" xmlns:xi="http://www.w3.org/2001/XInclude">\n')
|
|
542
|
+
f.write('<Domain>\n')
|
|
543
|
+
|
|
544
|
+
# one collection grid
|
|
545
|
+
f.write('<Grid\nCollectionType="Spatial"\nGridType="Collection"\nName="Collection">\n')
|
|
546
|
+
|
|
547
|
+
f.write('<Grid Name="coil" GridType="Uniform">\n')
|
|
548
|
+
f.write('<Topology NumberOfElements="' + str(len(coil_dipoles)) +
|
|
549
|
+
'" TopologyType="Polyvertex" Name="Tri">\n')
|
|
550
|
+
f.write('<DataItem Format="XML" Dimensions="' + str(len(coil_dipoles)) + ' 1">\n')
|
|
551
|
+
np.savetxt(f, list(range(len(coil_dipoles))), fmt='%d',
|
|
552
|
+
delimiter=' ') # 1 2 3 4 ... N_Points
|
|
553
|
+
f.write('</DataItem>\n')
|
|
554
|
+
f.write('</Topology>\n')
|
|
555
|
+
|
|
556
|
+
# nodes
|
|
557
|
+
f.write('<Geometry GeometryType="XYZ">\n')
|
|
558
|
+
f.write('<DataItem Format="HDF" Dimensions="' + str(len(coil_dipoles)) + ' 3">\n')
|
|
559
|
+
f.write(fn_hdf5_coilpos + ':' + '/dipoles\n')
|
|
560
|
+
f.write('</DataItem>\n')
|
|
561
|
+
f.write('</Geometry>\n')
|
|
562
|
+
|
|
563
|
+
f.write('</Grid>\n')
|
|
564
|
+
f.write('</Grid>\n')
|
|
565
|
+
f.write('</Domain>\n')
|
|
566
|
+
f.write('</Xdmf>\n')
|
|
567
|
+
|
|
568
|
+
# check hull and add to results
|
|
569
|
+
results.append(pynibs.in_hull(coil_dipoles, del_obj))
|
|
570
|
+
|
|
571
|
+
# find parameter which drives dipoles into head
|
|
572
|
+
combinations = np.array(combinations)
|
|
573
|
+
fail_params = []
|
|
574
|
+
if np.sum(results) > 0:
|
|
575
|
+
comb_idx = np.where(np.sum(results, axis=1) > 0)[0]
|
|
576
|
+
params_idx = np.where(np.var(combinations[comb_idx], axis=0) ==
|
|
577
|
+
min(np.var(combinations[comb_idx], axis=0)))[0]
|
|
578
|
+
for param_idx in params_idx:
|
|
579
|
+
comb_idx = comb_idx[combinations[comb_idx, params_idx[0]] != 1]
|
|
580
|
+
fail_params.append((param_idx, combinations[comb_idx, params_idx[0]][0]))
|
|
581
|
+
|
|
582
|
+
return fail_params
|
|
583
|
+
|
|
584
|
+
|
|
585
|
+
def create_stimsite_hdf5(fn_exp, fn_hdf, conditions_selected=None, sep="_", merge_sites=False, fix_angles=False,
|
|
586
|
+
data_dict=None, conditions_ignored=None):
|
|
587
|
+
"""
|
|
588
|
+
Reads results_conditions and creates an hdf5/xdmf pair with condition-wise centers of stimulation sites and
|
|
589
|
+
coil directions as data.
|
|
590
|
+
|
|
591
|
+
Parameters
|
|
592
|
+
----------
|
|
593
|
+
fn_exp : str
|
|
594
|
+
Path to results.csv.
|
|
595
|
+
fn_hdf : str
|
|
596
|
+
Path where to write file. Gets overridden if already existing.
|
|
597
|
+
conditions_selected : str or list of str, optional
|
|
598
|
+
List of conditions returned by the function, the others are omitted.
|
|
599
|
+
If None, all conditions are returned.
|
|
600
|
+
sep: str, default: "_"
|
|
601
|
+
Separator between condition label and angle (e.g. M1_0, or M1-0).
|
|
602
|
+
merge_sites : bool
|
|
603
|
+
If true, only one coil center per site is generated.
|
|
604
|
+
fix_angles : bool
|
|
605
|
+
rename 22.5 -> 0, 0 -> -45, 67.5 -> 90, 90 -> 135.
|
|
606
|
+
data_dict : dict ofnp.ndarray of float [n_stimsites] (optional), default: None
|
|
607
|
+
Dictionary containing data corresponding to the stimulation sites (keys).
|
|
608
|
+
conditions_ignored : str or list of str, optional
|
|
609
|
+
Conditions, which are not going to be included in the plot.
|
|
610
|
+
|
|
611
|
+
Returns
|
|
612
|
+
-------
|
|
613
|
+
<Files> : hdf5/xdmf file pair
|
|
614
|
+
Contains information about condition-wise stimulation sites and coil directions (fn_hdf)
|
|
615
|
+
|
|
616
|
+
Example
|
|
617
|
+
-------
|
|
618
|
+
.. code-block:: python
|
|
619
|
+
|
|
620
|
+
pynibs.create_stimsite_hdf5('/exp/1/experiment_corrected.csv',
|
|
621
|
+
'/stimsite', True, True)
|
|
622
|
+
"""
|
|
623
|
+
assert not fn_hdf.endswith('/')
|
|
624
|
+
|
|
625
|
+
exp = pynibs.read_csv(fn_exp)
|
|
626
|
+
|
|
627
|
+
exp_cond = pynibs.sort_by_condition(exp, conditions_selected=conditions_selected) # []
|
|
628
|
+
|
|
629
|
+
# get the unique conditions in the correct order
|
|
630
|
+
conds = [c['condition'][0] for c in exp_cond]
|
|
631
|
+
|
|
632
|
+
# remove conds
|
|
633
|
+
conds_temp = []
|
|
634
|
+
exp_cond_temp = []
|
|
635
|
+
|
|
636
|
+
if type(conditions_ignored) is not list:
|
|
637
|
+
conditions_ignored = [conditions_ignored]
|
|
638
|
+
|
|
639
|
+
for i_c, c in enumerate(conds):
|
|
640
|
+
ignore = False
|
|
641
|
+
for ci in list(conditions_ignored):
|
|
642
|
+
if c == ci:
|
|
643
|
+
ignore = True
|
|
644
|
+
|
|
645
|
+
if not ignore:
|
|
646
|
+
conds_temp.append(conds[i_c])
|
|
647
|
+
exp_cond_temp.append(exp_cond[i_c])
|
|
648
|
+
|
|
649
|
+
exp_cond = exp_cond_temp
|
|
650
|
+
conds = conds_temp
|
|
651
|
+
|
|
652
|
+
# hardcoded row #3 is condition
|
|
653
|
+
cond_idx = np.linspace(0, len(exp_cond), 1)[:, np.newaxis]
|
|
654
|
+
|
|
655
|
+
centers = []
|
|
656
|
+
m0 = []
|
|
657
|
+
m1 = []
|
|
658
|
+
m2 = []
|
|
659
|
+
|
|
660
|
+
for i_cond in range(len(exp_cond)):
|
|
661
|
+
centers.append(exp_cond[i_cond]['coil_mean_matrix'][0][0:3, 3])
|
|
662
|
+
m0.append(exp_cond[i_cond]['coil_mean_matrix'][0][0:3, 0])
|
|
663
|
+
m1.append(exp_cond[i_cond]['coil_mean_matrix'][0][0:3, 1])
|
|
664
|
+
m2.append(exp_cond[i_cond]['coil_mean_matrix'][0][0:3, 2])
|
|
665
|
+
|
|
666
|
+
# split conds to angles and sites: M1_90 -> M1, 90
|
|
667
|
+
angles = np.array([sp.split(sep)[-1] for sp in conds]).astype(np.float64)
|
|
668
|
+
sites = np.array([sp.split(sep)[0] for sp in conds])
|
|
669
|
+
sites_unique = np.unique(sites)
|
|
670
|
+
|
|
671
|
+
# average the center positions of a stimulation site over all orientations
|
|
672
|
+
if merge_sites:
|
|
673
|
+
|
|
674
|
+
# generate sites dict
|
|
675
|
+
centers_sites = dict()
|
|
676
|
+
|
|
677
|
+
for site in sites_unique:
|
|
678
|
+
centers_sites[site] = []
|
|
679
|
+
|
|
680
|
+
# gather all orientations and put them to the corresponding sites
|
|
681
|
+
for i_cond, site in enumerate(sites):
|
|
682
|
+
centers_sites[site].append(exp_cond[i_cond]['coil_mean_matrix'][0][0:3, 3])
|
|
683
|
+
|
|
684
|
+
# determine average position over all orientations for each site
|
|
685
|
+
for site in sites_unique:
|
|
686
|
+
centers_sites[site] = np.mean(np.vstack(centers_sites[site]), axis=0)
|
|
687
|
+
|
|
688
|
+
# write it back to centers
|
|
689
|
+
for i_cond, site in enumerate(sites):
|
|
690
|
+
centers[i_cond] = centers_sites[site]
|
|
691
|
+
|
|
692
|
+
centers = np.vstack(centers)
|
|
693
|
+
m0 = np.vstack(m0)
|
|
694
|
+
m1 = np.vstack(m1)
|
|
695
|
+
m2 = np.vstack(m2)
|
|
696
|
+
|
|
697
|
+
# enumerate sites, as paraview does not plot string array data
|
|
698
|
+
sites_idx = np.array(list(range(len(sites))))[:, np.newaxis]
|
|
699
|
+
|
|
700
|
+
angles[angles[:] == 675.] = 67.5
|
|
701
|
+
angles[angles[:] == 225.] = 22.5
|
|
702
|
+
|
|
703
|
+
if fix_angles:
|
|
704
|
+
# rename wrong angle names
|
|
705
|
+
angles_cor = np.copy(angles)
|
|
706
|
+
angles_cor[angles == 0] = -45.
|
|
707
|
+
angles_cor[angles == 22.5] = 0.
|
|
708
|
+
angles_cor[angles == 67.5] = 90.
|
|
709
|
+
angles_cor[angles == 90] = 135.
|
|
710
|
+
angles = angles_cor
|
|
711
|
+
|
|
712
|
+
# write hdf5 file
|
|
713
|
+
if not fn_hdf.endswith('.hdf5'):
|
|
714
|
+
fn_hdf += '.hdf5'
|
|
715
|
+
f = h5py.File(fn_hdf, 'w')
|
|
716
|
+
f.create_dataset('centers', data=centers.astype(np.float64))
|
|
717
|
+
f.create_dataset('m0', data=m0.astype(np.float64))
|
|
718
|
+
f.create_dataset('m1', data=m1.astype(np.float64))
|
|
719
|
+
f.create_dataset('m2', data=m2.astype(np.float64))
|
|
720
|
+
f.create_dataset('cond', data=np.string_(conds)) # this is a string array, not xdmf compatible
|
|
721
|
+
f.create_dataset('cond_idx', data=cond_idx)
|
|
722
|
+
f.create_dataset('angles', data=angles)
|
|
723
|
+
f.create_dataset('sites', data=np.string_(sites)) # this is a string array, not xdmf compatible
|
|
724
|
+
f.create_dataset('sites_idx', data=sites_idx)
|
|
725
|
+
|
|
726
|
+
data = None
|
|
727
|
+
if data_dict is not None:
|
|
728
|
+
data = np.zeros((len(list(data_dict.keys())), 1))
|
|
729
|
+
for i_data, cond in enumerate(conds):
|
|
730
|
+
data[i_data, 0] = data_dict[cond]
|
|
731
|
+
f.create_dataset('data', data=data)
|
|
732
|
+
f.close()
|
|
733
|
+
|
|
734
|
+
# write .xdmf file
|
|
735
|
+
f = open(fn_hdf[:-4] + 'xdmf', 'w')
|
|
736
|
+
fn_hdf = os.path.basename(fn_hdf) # relative links
|
|
737
|
+
|
|
738
|
+
# header
|
|
739
|
+
f.write('<?xml version="1.0"?>\n')
|
|
740
|
+
f.write('<!DOCTYPE Xdmf SYSTEM "Xdmf.dtd" []>\n')
|
|
741
|
+
f.write('<Xdmf Version="2.0" xmlns:xi="http://www.w3.org/2001/XInclude">\n')
|
|
742
|
+
f.write('<Domain>\n')
|
|
743
|
+
f.write('<Grid\nCollectionType="Spatial"\nGridType="Collection"\nName="Collection">\n')
|
|
744
|
+
|
|
745
|
+
# one grid for coil dipole nodes...store data hdf5.
|
|
746
|
+
#######################################################
|
|
747
|
+
f.write('<Grid Name="stimsites" GridType="Uniform">\n')
|
|
748
|
+
f.write('<Topology NumberOfElements="' + str(centers.shape[0]) +
|
|
749
|
+
'" TopologyType="Polyvertex" Name="Tri">\n')
|
|
750
|
+
f.write('<DataItem Format="XML" Dimensions="' + str(centers.shape[0]) + ' 1">\n')
|
|
751
|
+
# f.write(hdf5_fn + ':' + path + '/triangle_number_list\n')
|
|
752
|
+
np.savetxt(f, list(range(centers.shape[0])), fmt='%d', delimiter=' ') # 1 2 3 4 ... N_Points
|
|
753
|
+
f.write('</DataItem>\n')
|
|
754
|
+
f.write('</Topology>\n')
|
|
755
|
+
|
|
756
|
+
# nodes
|
|
757
|
+
f.write('<Geometry GeometryType="XYZ">\n')
|
|
758
|
+
f.write('<DataItem Format="HDF" Dimensions="' + str(centers.shape[0]) + ' 3">\n')
|
|
759
|
+
f.write(fn_hdf + ':' + '/centers\n')
|
|
760
|
+
f.write('</DataItem>\n')
|
|
761
|
+
f.write('</Geometry>\n')
|
|
762
|
+
|
|
763
|
+
# data
|
|
764
|
+
# dipole magnitude
|
|
765
|
+
# the 4 vectors
|
|
766
|
+
for i in range(3):
|
|
767
|
+
f.write('<Attribute Name="dir_' + str(i) + '" AttributeType="Vector" Center="Cell">\n')
|
|
768
|
+
f.write('<DataItem Format="HDF" Dimensions="' + str(centers.shape[0]) + ' 3">\n')
|
|
769
|
+
f.write(fn_hdf + ':' + '/m' + str(i) + '\n')
|
|
770
|
+
f.write('</DataItem>\n')
|
|
771
|
+
f.write('</Attribute>\n\n')
|
|
772
|
+
|
|
773
|
+
# angles
|
|
774
|
+
f.write('<Attribute Name="angles" AttributeType="Scalar" Center="Cell">\n')
|
|
775
|
+
f.write('<DataItem Format="HDF" Dimensions="' + str(centers.shape[0]) + ' 1">\n')
|
|
776
|
+
f.write(fn_hdf + ':' + '/angles\n')
|
|
777
|
+
f.write('</DataItem>\n')
|
|
778
|
+
f.write('</Attribute>\n\n')
|
|
779
|
+
|
|
780
|
+
# data
|
|
781
|
+
if data_dict is not None:
|
|
782
|
+
f.write('<Attribute Name="data" AttributeType="Scalar" Center="Cell">\n')
|
|
783
|
+
f.write('<DataItem Format="HDF" Dimensions="' + str(data.shape[0]) + ' 1">\n')
|
|
784
|
+
f.write(fn_hdf + ':' + '/data\n')
|
|
785
|
+
f.write('</DataItem>\n')
|
|
786
|
+
f.write('</Attribute>\n\n')
|
|
787
|
+
|
|
788
|
+
# site idx
|
|
789
|
+
f.write('<Attribute Name="sites_idx" AttributeType="Scalar" Center="Cell">\n')
|
|
790
|
+
f.write('<DataItem Format="HDF" Dimensions="' + str(centers.shape[0]) + ' 1">\n')
|
|
791
|
+
f.write(fn_hdf + ':' + '/sites_idx\n')
|
|
792
|
+
f.write('</DataItem>\n')
|
|
793
|
+
f.write('</Attribute>\n\n')
|
|
794
|
+
|
|
795
|
+
f.write('</Grid>\n')
|
|
796
|
+
# end coil dipole data
|
|
797
|
+
|
|
798
|
+
# footer
|
|
799
|
+
f.write('</Grid>\n')
|
|
800
|
+
f.write('</Domain>\n')
|
|
801
|
+
f.write('</Xdmf>\n')
|
|
802
|
+
f.close()
|
|
803
|
+
|
|
804
|
+
|
|
805
|
+
def create_stimsite_from_list(fn_hdf, poslist, datanames=None, data=None, overwrite=False):
|
|
806
|
+
"""
|
|
807
|
+
This takes a list of matsimnibs-style coil position and orientations and creates an .hdf5 + .xdmf tuple
|
|
808
|
+
for all positions.
|
|
809
|
+
|
|
810
|
+
Centers and coil orientations are written to disk, with optional data for each coil configuration.
|
|
811
|
+
|
|
812
|
+
Parameters
|
|
813
|
+
----------
|
|
814
|
+
fn_hdf: str
|
|
815
|
+
Filename for the .hdf5 file. The .xdmf is saved with the same basename.
|
|
816
|
+
Folder should already exist.
|
|
817
|
+
poslist: list of np.ndarray
|
|
818
|
+
(4,4) Positions.
|
|
819
|
+
datanames: str or list of str, optional
|
|
820
|
+
Dataset names for ``data``.
|
|
821
|
+
data: np.ndarray, optional
|
|
822
|
+
Dataset array with shape = ``(len(poslist.pos), len(datanames())``.
|
|
823
|
+
overwrite : bool, defaul: False
|
|
824
|
+
Overwrite existing files.
|
|
825
|
+
"""
|
|
826
|
+
centers = []
|
|
827
|
+
m0 = []
|
|
828
|
+
m1 = []
|
|
829
|
+
m2 = []
|
|
830
|
+
if data is not None:
|
|
831
|
+
assert isinstance(data, np.ndarray)
|
|
832
|
+
|
|
833
|
+
for lst in poslist:
|
|
834
|
+
centers.append(lst[0:3, 3])
|
|
835
|
+
m0.append(lst[0:3, 0])
|
|
836
|
+
m1.append(lst[0:3, 1])
|
|
837
|
+
m2.append(lst[0:3, 2])
|
|
838
|
+
|
|
839
|
+
centers = np.vstack(centers)
|
|
840
|
+
m0 = np.vstack(m0)
|
|
841
|
+
m1 = np.vstack(m1)
|
|
842
|
+
m2 = np.vstack(m2)
|
|
843
|
+
|
|
844
|
+
write_coil_pos_hdf5(fn_hdf, centers, m0, m1, m2, datanames=datanames, data=data, overwrite=overwrite)
|
|
845
|
+
|
|
846
|
+
|
|
847
|
+
def create_stimsite_from_tmslist(fn_hdf, poslist, datanames=None, data=None, overwrite=False):
|
|
848
|
+
"""
|
|
849
|
+
This takes a :py:class:simnibs.sim_struct.TMSLIST from simnibs and creates an .hdf5 + .xdmf tuple for all positions.
|
|
850
|
+
|
|
851
|
+
Centers and coil orientations are written to disk, with optional data for each coil configuration.
|
|
852
|
+
|
|
853
|
+
Parameters
|
|
854
|
+
----------
|
|
855
|
+
fn_hdf: str
|
|
856
|
+
Filename for the .hdf5 file. The .xdmf is saved with the same basename.
|
|
857
|
+
Folder should already exist.
|
|
858
|
+
poslist: simnibs.sim_struct.TMSLIST
|
|
859
|
+
poslist.pos[*].matsimnibs have to be set.
|
|
860
|
+
datanames: str or list of str, optional
|
|
861
|
+
Dataset names for ``data``.
|
|
862
|
+
data: np.ndarray, optional
|
|
863
|
+
Dataset array with shape = ``(len(poslist.pos), len(datanames())``.
|
|
864
|
+
overwrite : bool, default: False
|
|
865
|
+
Overwrite existing files
|
|
866
|
+
"""
|
|
867
|
+
centers = []
|
|
868
|
+
m0 = []
|
|
869
|
+
m1 = []
|
|
870
|
+
m2 = []
|
|
871
|
+
assert poslist.pos
|
|
872
|
+
if data is not None:
|
|
873
|
+
assert isinstance(data, np.ndarray)
|
|
874
|
+
for pos in poslist.pos:
|
|
875
|
+
assert pos.matsimnibs is not None
|
|
876
|
+
pos.matsimnibs = np.array(pos.matsimnibs)
|
|
877
|
+
centers.append(pos.matsimnibs[0:3, 3])
|
|
878
|
+
m0.append(pos.matsimnibs[0:3, 0])
|
|
879
|
+
m1.append(pos.matsimnibs[0:3, 1])
|
|
880
|
+
m2.append(pos.matsimnibs[0:3, 2])
|
|
881
|
+
|
|
882
|
+
centers = np.vstack(centers)
|
|
883
|
+
m0 = np.vstack(m0)
|
|
884
|
+
m1 = np.vstack(m1)
|
|
885
|
+
m2 = np.vstack(m2)
|
|
886
|
+
|
|
887
|
+
write_coil_pos_hdf5(fn_hdf, centers, m0, m1, m2, datanames=datanames, data=data, overwrite=overwrite)
|
|
888
|
+
|
|
889
|
+
|
|
890
|
+
def create_stimsite_from_exp_hdf5(fn_exp, fn_hdf, datanames=None, data=None, overwrite=False):
|
|
891
|
+
"""
|
|
892
|
+
This takes an experiment.hdf5 file and creates an .hdf5 + .xdmf tuple for all coil positions for visualization.
|
|
893
|
+
|
|
894
|
+
Parameters
|
|
895
|
+
----------
|
|
896
|
+
fn_exp : str
|
|
897
|
+
Path to experiment.hdf5
|
|
898
|
+
fn_hdf : str
|
|
899
|
+
Filename for the resulting .hdf5 file. The .xdmf is saved with the same basename.
|
|
900
|
+
Folder should already exist.
|
|
901
|
+
datanames : str or list of str, optional
|
|
902
|
+
Dataset names for ``data``
|
|
903
|
+
data : np.ndarray, optional
|
|
904
|
+
Dataset array with shape = ``(len(poslist.pos), len(datanames())``.
|
|
905
|
+
overwrite : bool, default: False
|
|
906
|
+
Overwrite existing files.
|
|
907
|
+
"""
|
|
908
|
+
df_stim = pd.read_hdf(fn_exp, "stim_data")
|
|
909
|
+
|
|
910
|
+
matsimnibs = np.zeros((4, 4, df_stim.shape[0]))
|
|
911
|
+
|
|
912
|
+
for i in range(df_stim.shape[0]):
|
|
913
|
+
matsimnibs[:, :, i] = df_stim["coil_mean"].iloc[i]
|
|
914
|
+
|
|
915
|
+
create_stimsite_from_matsimnibs(fn_hdf=fn_hdf,
|
|
916
|
+
matsimnibs=matsimnibs,
|
|
917
|
+
datanames=datanames,
|
|
918
|
+
data=data,
|
|
919
|
+
overwrite=overwrite)
|
|
920
|
+
|
|
921
|
+
|
|
922
|
+
def create_stimsite_from_matsimnibs(fn_hdf, matsimnibs, datanames=None, data=None, overwrite=False):
|
|
923
|
+
"""
|
|
924
|
+
This takes a matsimnibs array and creates an .hdf5 + .xdmf tuple for all coil positions for visualization.
|
|
925
|
+
|
|
926
|
+
Centers and coil orientations are written disk.
|
|
927
|
+
|
|
928
|
+
Parameters
|
|
929
|
+
----------
|
|
930
|
+
fn_hdf: str
|
|
931
|
+
Filename for the .hdf5 file. The .xdmf is saved with the same basename.
|
|
932
|
+
Folder should already exist.
|
|
933
|
+
matsimnibs: np.ndarray
|
|
934
|
+
(4, 4, n_pos)
|
|
935
|
+
Matsimnibs matrices containing the coil orientation (x,y,z) and position (p)
|
|
936
|
+
|
|
937
|
+
.. math::
|
|
938
|
+
\\begin{bmatrix}
|
|
939
|
+
| & | & | & | \\\\
|
|
940
|
+
x & y & z & p \\\\
|
|
941
|
+
| & | & | & | \\\\
|
|
942
|
+
0 & 0 & 0 & 1 \\\\
|
|
943
|
+
\\end{bmatrix}
|
|
944
|
+
datanames: str or list of str, optional
|
|
945
|
+
Dataset names for ``data``.
|
|
946
|
+
data: np.ndarray, optional
|
|
947
|
+
(len(poslist.pos), len(datanames).
|
|
948
|
+
overwrite : bool, default: False
|
|
949
|
+
Overwrite existing files.
|
|
950
|
+
"""
|
|
951
|
+
matsimnibs = np.atleast_3d(matsimnibs)
|
|
952
|
+
n_pos = matsimnibs.shape[2]
|
|
953
|
+
centers = np.zeros((n_pos, 3))
|
|
954
|
+
m0 = np.zeros((n_pos, 3))
|
|
955
|
+
m1 = np.zeros((n_pos, 3))
|
|
956
|
+
m2 = np.zeros((n_pos, 3))
|
|
957
|
+
if data is not None:
|
|
958
|
+
assert isinstance(data, np.ndarray)
|
|
959
|
+
|
|
960
|
+
for i in range(matsimnibs.shape[2]):
|
|
961
|
+
centers[i, :] = matsimnibs[0:3, 3, i]
|
|
962
|
+
m0[i, :] = matsimnibs[0:3, 0, i]
|
|
963
|
+
m1[i, :] = matsimnibs[0:3, 1, i]
|
|
964
|
+
m2[i, :] = matsimnibs[0:3, 2, i]
|
|
965
|
+
|
|
966
|
+
write_coil_pos_hdf5(fn_hdf, centers, m0, m1, m2, datanames=datanames, data=data, overwrite=overwrite)
|
|
967
|
+
|
|
968
|
+
|
|
969
|
+
def write_coil_pos_hdf5(fn_hdf, centers, m0, m1, m2, datanames=None, data=None, overwrite=False):
|
|
970
|
+
"""
|
|
971
|
+
Creates a ``.hdf5`` + ``.xdmf`` file tuple for all coil positions.
|
|
972
|
+
Coil centers and coil orientations are saved, and - optionally - data for each position if ``data`` and
|
|
973
|
+
``datanames`` are provided.
|
|
974
|
+
|
|
975
|
+
Parameters
|
|
976
|
+
----------
|
|
977
|
+
fn_hdf : str
|
|
978
|
+
Filename for the .hdf5 file. The .xdmf is saved with the same basename.
|
|
979
|
+
Folder should already exist.
|
|
980
|
+
centers : np.ndarray of float
|
|
981
|
+
(n_pos, 3) Coil positions.
|
|
982
|
+
m0 : np.ndarray of float
|
|
983
|
+
(n_pos, 3) Coil orientation x-axis (looking at the active (patient) side of the coil pointing to the right).
|
|
984
|
+
m1 : np.ndarray of float
|
|
985
|
+
(n_pos, 3) Coil orientation y-axis (looking at the active side of the coil pointing up away from the handle).
|
|
986
|
+
m2 : np.ndarray of float
|
|
987
|
+
(n_pos, 3) Coil orientation z-axis (looking at the active (patient) side of the coil pointing to the patient).
|
|
988
|
+
datanames : str or list of str, optional
|
|
989
|
+
(n_data) Dataset names for ``data``
|
|
990
|
+
data : np.ndarray, optional
|
|
991
|
+
(n_pos, n_data) Dataset array with (len(poslist.pos), len(datanames()).
|
|
992
|
+
overwrite : bool, default: False
|
|
993
|
+
Overwrite existing files.
|
|
994
|
+
"""
|
|
995
|
+
n_pos = centers.shape[0]
|
|
996
|
+
if isinstance(datanames, str):
|
|
997
|
+
datanames = [datanames]
|
|
998
|
+
|
|
999
|
+
if data is not None:
|
|
1000
|
+
if datanames is None:
|
|
1001
|
+
raise ValueError("Provide datanames= with data= argument.")
|
|
1002
|
+
if isinstance(datanames, str):
|
|
1003
|
+
datanames = [datanames]
|
|
1004
|
+
if len(data.shape) <= 1:
|
|
1005
|
+
data = np.atleast_1d(data)[:, np.newaxis]
|
|
1006
|
+
assert data.shape == (n_pos, len(datanames))
|
|
1007
|
+
if datanames is not None and data is None:
|
|
1008
|
+
raise ValueError("Provide data= with datanames= argument.")
|
|
1009
|
+
|
|
1010
|
+
m0_reshaped = np.hstack((m0, np.zeros((n_pos, 1)))).T[:, np.newaxis, :]
|
|
1011
|
+
m1_reshaped = np.hstack((m1, np.zeros((n_pos, 1)))).T[:, np.newaxis, :]
|
|
1012
|
+
m2_reshaped = np.hstack((m2, np.zeros((n_pos, 1)))).T[:, np.newaxis, :]
|
|
1013
|
+
centers_reshaped = np.hstack((centers, np.ones((n_pos, 1)))).T[:, np.newaxis, :]
|
|
1014
|
+
|
|
1015
|
+
matsimnibs = np.concatenate((m0_reshaped, m1_reshaped, m2_reshaped, centers_reshaped), axis=1)
|
|
1016
|
+
|
|
1017
|
+
# write hdf5 file
|
|
1018
|
+
if not fn_hdf.endswith('.hdf5'):
|
|
1019
|
+
fn_hdf += '.hdf5'
|
|
1020
|
+
if os.path.exists(fn_hdf) and not overwrite:
|
|
1021
|
+
raise OSError(fn_hdf + " already exists. Set overwrite flag for create_stimsite_from_poslist.")
|
|
1022
|
+
|
|
1023
|
+
with h5py.File(fn_hdf, 'w') as f:
|
|
1024
|
+
f.create_dataset('centers', data=centers.astype(np.float64))
|
|
1025
|
+
f.create_dataset('m0', data=m0.astype(np.float64))
|
|
1026
|
+
f.create_dataset('m1', data=m1.astype(np.float64))
|
|
1027
|
+
f.create_dataset('m2', data=m2.astype(np.float64))
|
|
1028
|
+
f.create_dataset("matsimnibs", data=matsimnibs)
|
|
1029
|
+
|
|
1030
|
+
if data is not None:
|
|
1031
|
+
for i, col in enumerate(data.T):
|
|
1032
|
+
f.create_dataset('/data/' + datanames[i], data=col)
|
|
1033
|
+
|
|
1034
|
+
# write .xdmf file
|
|
1035
|
+
with open(fn_hdf[:-4] + 'xdmf', 'w') as f:
|
|
1036
|
+
fn_hdf = os.path.basename(fn_hdf) # relative links
|
|
1037
|
+
|
|
1038
|
+
# header
|
|
1039
|
+
f.write('<?xml version="1.0"?>\n')
|
|
1040
|
+
f.write('<!DOCTYPE Xdmf SYSTEM "Xdmf.dtd" []>\n')
|
|
1041
|
+
f.write('<Xdmf Version="2.0" xmlns:xi="http://www.w3.org/2001/XInclude">\n')
|
|
1042
|
+
f.write('<Domain>\n')
|
|
1043
|
+
f.write('<Grid CollectionType="Spatial" GridType="Collection" Name="Collection">\n')
|
|
1044
|
+
|
|
1045
|
+
# one grid for coil dipole nodes...store data hdf5.
|
|
1046
|
+
#######################################################
|
|
1047
|
+
f.write('<Grid Name="stimsites" GridType="Uniform">\n')
|
|
1048
|
+
f.write(f'<Topology NumberOfElements="{centers.shape[0]}" TopologyType="Polyvertex" Name="Tri">\n')
|
|
1049
|
+
f.write(f'\t<DataItem Format="XML" Dimensions="{centers.shape[0]} 1">\n')
|
|
1050
|
+
np.savetxt(f, list(range(centers.shape[0])), fmt='\t%d', delimiter=' ') # 1 2 3 4 ... N_Points
|
|
1051
|
+
f.write('\t</DataItem>\n')
|
|
1052
|
+
f.write('</Topology>\n\n')
|
|
1053
|
+
|
|
1054
|
+
# nodes
|
|
1055
|
+
f.write('<Geometry GeometryType="XYZ">\n')
|
|
1056
|
+
f.write(f'\t<DataItem Format="HDF" Dimensions="{centers.shape[0]} 3">\n')
|
|
1057
|
+
f.write(f'\t{fn_hdf}:/centers\n')
|
|
1058
|
+
f.write('\t</DataItem>\n')
|
|
1059
|
+
f.write('</Geometry>\n\n')
|
|
1060
|
+
|
|
1061
|
+
# data
|
|
1062
|
+
# dipole magnitude
|
|
1063
|
+
# the 4 vectors
|
|
1064
|
+
for i in range(3):
|
|
1065
|
+
f.write(f'\t\t<Attribute Name="dir_{i}" AttributeType="Vector" Center="Cell">\n')
|
|
1066
|
+
f.write(f'\t\t\t<DataItem Format="HDF" Dimensions="{centers.shape[0]} 3">\n')
|
|
1067
|
+
f.write(f'\t\t\t{fn_hdf}:/m{i}\n')
|
|
1068
|
+
f.write('\t\t\t</DataItem>\n')
|
|
1069
|
+
f.write('\t\t</Attribute>\n\n')
|
|
1070
|
+
|
|
1071
|
+
if data is not None:
|
|
1072
|
+
for i, col in enumerate(data.T):
|
|
1073
|
+
f.write(f'\t\t<Attribute Name="{datanames[i]}" AttributeType="Scalar" Center="Cell">\n')
|
|
1074
|
+
f.write('\t\t\t<DataItem Format="HDF" Dimensions="' + str(centers.shape[0]) + ' 1">\n')
|
|
1075
|
+
f.write(f'\t\t\t{fn_hdf}:/data/{datanames[i]}\n')
|
|
1076
|
+
f.write('\t\t\t</DataItem>\n')
|
|
1077
|
+
f.write('\t\t</Attribute>\n\n')
|
|
1078
|
+
|
|
1079
|
+
f.write('</Grid>\n')
|
|
1080
|
+
# end coil dipole data
|
|
1081
|
+
|
|
1082
|
+
# footer
|
|
1083
|
+
f.write('</Grid>\n')
|
|
1084
|
+
f.write('</Domain>\n')
|
|
1085
|
+
f.write('</Xdmf>\n')
|
|
1086
|
+
|
|
1087
|
+
|
|
1088
|
+
def sort_opt_coil_positions(fn_coil_pos_opt, fn_coil_pos, fn_out_hdf5=None, root_path="/0/0/", verbose=False,
|
|
1089
|
+
print_output=False):
|
|
1090
|
+
"""
|
|
1091
|
+
Sorts coil positions according to Traveling Salesman problem
|
|
1092
|
+
|
|
1093
|
+
Parameters
|
|
1094
|
+
----------
|
|
1095
|
+
fn_coil_pos_opt : str
|
|
1096
|
+
Name of .hdf5 file containing the optimal coil position indices
|
|
1097
|
+
fn_coil_pos : str
|
|
1098
|
+
Name of .hdf5 file containing the matsimnibs matrices of all coil positions
|
|
1099
|
+
fn_out_hdf5 : str
|
|
1100
|
+
Name of output .hdf5 file (will be saved in the same format as fn_coil_pos_opt)
|
|
1101
|
+
verbose : bool, default: False
|
|
1102
|
+
Print output messages
|
|
1103
|
+
print_output : bool or str, default: False
|
|
1104
|
+
Print output image as .png file showing optimal path
|
|
1105
|
+
|
|
1106
|
+
Returns
|
|
1107
|
+
-------
|
|
1108
|
+
<file> .hdf5 file containing the sorted optimal coil position indices
|
|
1109
|
+
"""
|
|
1110
|
+
from ortools.constraint_solver import routing_enums_pb2
|
|
1111
|
+
from ortools.constraint_solver import pywrapcp
|
|
1112
|
+
|
|
1113
|
+
if verbose:
|
|
1114
|
+
print(f"Loading optimal coil indices from file: {fn_coil_pos_opt}")
|
|
1115
|
+
|
|
1116
|
+
with_intensities = False
|
|
1117
|
+
with h5py.File(fn_coil_pos_opt, "r") as f:
|
|
1118
|
+
coil_pos_idx = f[root_path + "coil_seq"][:, 0].astype(int)
|
|
1119
|
+
goal_fun_val = f[root_path + "coil_seq"][:, 1]
|
|
1120
|
+
|
|
1121
|
+
with_intensities = f[root_path + "coil_seq"][:].shape[1] == 3
|
|
1122
|
+
if with_intensities:
|
|
1123
|
+
intensities_opt = f[root_path + "coil_seq"][:, 2]
|
|
1124
|
+
|
|
1125
|
+
if verbose:
|
|
1126
|
+
print(f"Loading all coil positions from file: {fn_coil_pos}")
|
|
1127
|
+
|
|
1128
|
+
with h5py.File(fn_coil_pos, "r") as f:
|
|
1129
|
+
matsimnibs = f["matsimnibs"][:]
|
|
1130
|
+
|
|
1131
|
+
matsimnibs_opt = np.zeros((4, 4, len(coil_pos_idx)))
|
|
1132
|
+
|
|
1133
|
+
for i, idx in enumerate(coil_pos_idx):
|
|
1134
|
+
matsimnibs_opt[:, :, i] = matsimnibs[:, :, idx]
|
|
1135
|
+
|
|
1136
|
+
opt_idx_sort = np.argsort(matsimnibs_opt[0, 3, :])
|
|
1137
|
+
matsimnibs_opt = matsimnibs_opt[:, :, opt_idx_sort]
|
|
1138
|
+
|
|
1139
|
+
unique_idx = np.zeros(len(opt_idx_sort))
|
|
1140
|
+
|
|
1141
|
+
pos_unique = np.unique(matsimnibs_opt[0, 3, :])
|
|
1142
|
+
|
|
1143
|
+
for i, pos in enumerate(pos_unique):
|
|
1144
|
+
unique_idx[matsimnibs_opt[0, 3, :] == pos] = i
|
|
1145
|
+
|
|
1146
|
+
if verbose:
|
|
1147
|
+
print(f"Sorting same positions according to angles ...")
|
|
1148
|
+
|
|
1149
|
+
matsimnibs_opt_sorted_pert = np.zeros(matsimnibs_opt.shape)
|
|
1150
|
+
matsimnibs_opt_sorted = np.zeros(matsimnibs_opt.shape)
|
|
1151
|
+
|
|
1152
|
+
for i in unique_idx:
|
|
1153
|
+
pos = matsimnibs_opt[:, :, unique_idx == i]
|
|
1154
|
+
angles = np.zeros(pos.shape[2])
|
|
1155
|
+
|
|
1156
|
+
if pos.shape[2] > 1:
|
|
1157
|
+
for i_p in range(pos.shape[2]):
|
|
1158
|
+
dot_prod = np.dot(pos[:3, 0, 0], pos[:3, 0, i_p])
|
|
1159
|
+
if dot_prod > 1:
|
|
1160
|
+
dot_prod = 1
|
|
1161
|
+
elif dot_prod < -1:
|
|
1162
|
+
dot_prod = -1
|
|
1163
|
+
angles[i_p] = np.arccos(dot_prod) / np.pi * 180
|
|
1164
|
+
|
|
1165
|
+
angles_sort_idx = np.argsort(angles)
|
|
1166
|
+
pos_sorted = pos[:, :, angles_sort_idx]
|
|
1167
|
+
else:
|
|
1168
|
+
pos_sorted = pos
|
|
1169
|
+
|
|
1170
|
+
matsimnibs_opt_sorted[:, :, unique_idx == i] = pos_sorted
|
|
1171
|
+
matsimnibs_opt_sorted_pert[:, :, unique_idx == i] = pos_sorted
|
|
1172
|
+
matsimnibs_opt_sorted_pert[0, 3, unique_idx == i] += np.arange(len(angles)) * 1e-2
|
|
1173
|
+
|
|
1174
|
+
coords = matsimnibs_opt_sorted_pert[:3, 3, :].transpose()
|
|
1175
|
+
coords_list = [tuple(c) for c in coords]
|
|
1176
|
+
|
|
1177
|
+
def compute_euclidean_distance_matrix(locations):
|
|
1178
|
+
"""Creates callback to return distance between points."""
|
|
1179
|
+
distances = {}
|
|
1180
|
+
for from_counter, from_node in enumerate(locations):
|
|
1181
|
+
distances[from_counter] = {}
|
|
1182
|
+
for to_counter, to_node in enumerate(locations):
|
|
1183
|
+
if from_counter == to_counter:
|
|
1184
|
+
distances[from_counter][to_counter] = 0
|
|
1185
|
+
else:
|
|
1186
|
+
# Euclidean distance
|
|
1187
|
+
distances[from_counter][to_counter] = (int(
|
|
1188
|
+
math.hypot((from_node[0] - to_node[0]),
|
|
1189
|
+
(from_node[1] - to_node[1]))))
|
|
1190
|
+
return distances
|
|
1191
|
+
|
|
1192
|
+
def get_routes(s, r, m):
|
|
1193
|
+
"""Get vehicle routes from a solution and store them in an array."""
|
|
1194
|
+
# Get vehicle routes and store them in a two-dimensional array whose
|
|
1195
|
+
# i,j entry is the jth location visited by vehicle i along its route.
|
|
1196
|
+
routes_l = []
|
|
1197
|
+
for route_nbr in range(r.vehicles()):
|
|
1198
|
+
index = r.Start(route_nbr)
|
|
1199
|
+
route = [m.IndexToNode(index)]
|
|
1200
|
+
while not r.IsEnd(index):
|
|
1201
|
+
index = s.Value(r.NextVar(index))
|
|
1202
|
+
route.append(m.IndexToNode(index))
|
|
1203
|
+
routes_l.append(route)
|
|
1204
|
+
|
|
1205
|
+
return routes_l
|
|
1206
|
+
|
|
1207
|
+
def distance_callback(from_index, to_index):
|
|
1208
|
+
"""Returns the distance between the two nodes."""
|
|
1209
|
+
# Convert from routing variable Index to distance matrix NodeIndex.
|
|
1210
|
+
from_node = manager.IndexToNode(from_index)
|
|
1211
|
+
to_node = manager.IndexToNode(to_index)
|
|
1212
|
+
return distance_matrix[from_node][to_node]
|
|
1213
|
+
|
|
1214
|
+
# Instantiate the data problem.
|
|
1215
|
+
data = {}
|
|
1216
|
+
# Locations in block units
|
|
1217
|
+
data['locations'] = coords_list
|
|
1218
|
+
data['num_vehicles'] = 1
|
|
1219
|
+
data['depot'] = 0
|
|
1220
|
+
|
|
1221
|
+
# Create the routing index manager.
|
|
1222
|
+
manager = pywrapcp.RoutingIndexManager(len(data['locations']), data['num_vehicles'], data['depot'])
|
|
1223
|
+
|
|
1224
|
+
# Create Routing Model.
|
|
1225
|
+
routing = pywrapcp.RoutingModel(manager)
|
|
1226
|
+
distance_matrix = compute_euclidean_distance_matrix(data['locations'])
|
|
1227
|
+
transit_callback_index = routing.RegisterTransitCallback(distance_callback)
|
|
1228
|
+
|
|
1229
|
+
# Define cost of each arc.
|
|
1230
|
+
routing.SetArcCostEvaluatorOfAllVehicles(transit_callback_index)
|
|
1231
|
+
|
|
1232
|
+
# Setting first solution heuristic.
|
|
1233
|
+
search_parameters = pywrapcp.DefaultRoutingSearchParameters()
|
|
1234
|
+
search_parameters.first_solution_strategy = routing_enums_pb2.FirstSolutionStrategy.PATH_CHEAPEST_ARC
|
|
1235
|
+
|
|
1236
|
+
# Solve the problem.
|
|
1237
|
+
if verbose:
|
|
1238
|
+
print(f"Determine optimal order of sequence ...")
|
|
1239
|
+
solution = routing.SolveWithParameters(search_parameters)
|
|
1240
|
+
|
|
1241
|
+
routes = np.array(get_routes(solution, routing, manager)[0])[:-1]
|
|
1242
|
+
|
|
1243
|
+
matsimnibs_opt_sorted = matsimnibs_opt_sorted[:, :, routes]
|
|
1244
|
+
|
|
1245
|
+
idx_global_sorted = np.zeros(len(goal_fun_val))
|
|
1246
|
+
goal_fun_val_sorted = np.zeros(len(goal_fun_val))
|
|
1247
|
+
if with_intensities:
|
|
1248
|
+
intensities_opt_sorted = np.zeros(len(intensities_opt))
|
|
1249
|
+
|
|
1250
|
+
for i in range(len(idx_global_sorted)):
|
|
1251
|
+
for j in range(matsimnibs.shape[2]):
|
|
1252
|
+
if (matsimnibs[:, :, j] == matsimnibs_opt_sorted[:, :, i]).all():
|
|
1253
|
+
idx_global_sorted[i] = int(j)
|
|
1254
|
+
goal_fun_val_sorted[i] = goal_fun_val[np.where(int(idx_global_sorted[i]) == coil_pos_idx)[0][0]]
|
|
1255
|
+
if with_intensities:
|
|
1256
|
+
intensities_opt_sorted[i] = intensities_opt[np.where(int(idx_global_sorted[i]) == coil_pos_idx)[0][0]]
|
|
1257
|
+
|
|
1258
|
+
# overwrite input file if no output file given
|
|
1259
|
+
outpath = fn_coil_pos_opt
|
|
1260
|
+
if fn_out_hdf5 is not None:
|
|
1261
|
+
outpath = fn_out_hdf5
|
|
1262
|
+
shutil.copy(fn_coil_pos_opt, fn_out_hdf5)
|
|
1263
|
+
|
|
1264
|
+
if verbose:
|
|
1265
|
+
print(f"Saving output to: {outpath}")
|
|
1266
|
+
|
|
1267
|
+
with h5py.File(outpath, "a") as f:
|
|
1268
|
+
if with_intensities:
|
|
1269
|
+
f[root_path + "coil_seq"][:] = np.vstack((idx_global_sorted, goal_fun_val_sorted,
|
|
1270
|
+
intensities_opt_sorted)).transpose()
|
|
1271
|
+
else:
|
|
1272
|
+
f[root_path + "coil_seq"][:] = np.vstack((idx_global_sorted, goal_fun_val_sorted)).transpose()
|
|
1273
|
+
|
|
1274
|
+
if print_output:
|
|
1275
|
+
fig = plt.figure()
|
|
1276
|
+
ax = fig.add_subplot(projection='3d')
|
|
1277
|
+
if with_intensities:
|
|
1278
|
+
ax.scatter(coords[routes, 0], coords[routes, 1], coords[routes, 2], s=intensities_opt_sorted * 11, c="k")
|
|
1279
|
+
else:
|
|
1280
|
+
ax.scatter(coords[routes, 0], coords[routes, 1], coords[routes, 2], c="k")
|
|
1281
|
+
ax.plot3D(coords[routes, 0], coords[routes, 1], coords[routes, 2])
|
|
1282
|
+
ax.set_xlabel("x in mm")
|
|
1283
|
+
ax.set_ylabel("y in mm")
|
|
1284
|
+
ax.set_zlabel("z in mm")
|
|
1285
|
+
ax.view_init(elev=40., azim=230)
|
|
1286
|
+
plt.savefig(print_output, dpi=600, transparent=True)
|
|
1287
|
+
|
|
1288
|
+
|
|
1289
|
+
def random_walk_coil(start_mat, n_steps, fn_mesh_hdf5, angles_dev=3, distance_low=1, distance_high=4,
|
|
1290
|
+
angles_metric='deg', coil_pos_fn=None):
|
|
1291
|
+
"""
|
|
1292
|
+
Computes random walk coil positions/orientations for a SimNIBS matsimnibs coil pos/ori.
|
|
1293
|
+
|
|
1294
|
+
Parameters
|
|
1295
|
+
----------
|
|
1296
|
+
start_mat : np.ndarry
|
|
1297
|
+
(4, 4) SimNIBS matsimnibs.
|
|
1298
|
+
n_steps : int
|
|
1299
|
+
Number of steps to walk.
|
|
1300
|
+
fn_mesh_hdf5 : str
|
|
1301
|
+
.hdf5 mesh filename, used to compute skin-coil distances.
|
|
1302
|
+
angles_dev : float or list of float, default: 3
|
|
1303
|
+
Angles deviation,`` np.random.normal(scale=angles_dev)``. If list, angles_dev = [alpha, beta, theta].
|
|
1304
|
+
distance_low : float, default: 1
|
|
1305
|
+
Minimum skin-coil distance.
|
|
1306
|
+
distance_high : float, default: 4
|
|
1307
|
+
Maximum skin-coil distance.
|
|
1308
|
+
angles_metric : str, default: 'deg'
|
|
1309
|
+
One of ``('deg', 'rad')``.
|
|
1310
|
+
coil_pos_fn : str, optional
|
|
1311
|
+
If provided, .hdf5/.xdmf tuple is written with coil positions/orientations.
|
|
1312
|
+
|
|
1313
|
+
Returns
|
|
1314
|
+
-------
|
|
1315
|
+
walked_coils : np.ndarray
|
|
1316
|
+
(4, 4, n_steps + 1) coil positions / orientations.
|
|
1317
|
+
<file> : .hdf5/.xdmf file tupel with n_steps + 1 coil positions/orientations.
|
|
1318
|
+
"""
|
|
1319
|
+
angles_dev = np.atleast_1d(np.squeeze(np.array(angles_dev)))
|
|
1320
|
+
if angles_dev.shape[0] == 1:
|
|
1321
|
+
angles_dev = np.repeat(angles_dev, 3)
|
|
1322
|
+
assert angles_dev.shape == (3,)
|
|
1323
|
+
|
|
1324
|
+
mats = [start_mat]
|
|
1325
|
+
for i in range(n_steps):
|
|
1326
|
+
# walk position
|
|
1327
|
+
mat = mats[-1].copy()
|
|
1328
|
+
mat[:3, 3] = np.random.normal(loc=mat[:3, 3], scale=.3)
|
|
1329
|
+
|
|
1330
|
+
mat_rot = start_mat.copy()
|
|
1331
|
+
|
|
1332
|
+
# walk angles
|
|
1333
|
+
for idx, axis in enumerate(['x', 'y', 'z']):
|
|
1334
|
+
if angles_dev[idx] != 0:
|
|
1335
|
+
mat_rot = pynibs.rotate_matsimnibs_euler(axis=axis,
|
|
1336
|
+
angle=np.random.normal(scale=angles_dev[idx]),
|
|
1337
|
+
matsimnibs=mat_rot,
|
|
1338
|
+
metric=angles_metric)
|
|
1339
|
+
mat[:3, :3] = mat_rot[:3, :3]
|
|
1340
|
+
mats.append(mat)
|
|
1341
|
+
|
|
1342
|
+
mats = np.moveaxis(np.array(mats), 0, -1)
|
|
1343
|
+
|
|
1344
|
+
# walk skin-coil distance
|
|
1345
|
+
assert distance_low >= 0 and distance_high >= 0
|
|
1346
|
+
assert distance_high >= distance_low
|
|
1347
|
+
distances = np.random.uniform(low=distance_low, high=distance_high, size=n_steps + 1)
|
|
1348
|
+
mats = pynibs.coil_distance_correction_matsimnibs(matsimnibs=mats,
|
|
1349
|
+
fn_mesh_hdf5=fn_mesh_hdf5,
|
|
1350
|
+
distance=distances)
|
|
1351
|
+
|
|
1352
|
+
if coil_pos_fn is not None:
|
|
1353
|
+
pynibs.write_coil_pos_hdf5(fn_hdf=os.path.splitext(coil_pos_fn)[0],
|
|
1354
|
+
centers=mats[:3, 3, :].T,
|
|
1355
|
+
m0=mats[:3, 0, :].T,
|
|
1356
|
+
m1=mats[:3, 1, :].T,
|
|
1357
|
+
m2=mats[:3, 2, :].T,
|
|
1358
|
+
overwrite=True)
|
|
1359
|
+
|
|
1360
|
+
pynibs.write_coil_sequence_xdmf(coil_pos_fn,
|
|
1361
|
+
np.arange(n_steps + 1),
|
|
1362
|
+
vec1=mats[:3, 0, :].T,
|
|
1363
|
+
vec2=mats[:3, 1, :].T,
|
|
1364
|
+
vec3=mats[:3, 2, :].T,
|
|
1365
|
+
output_xdmf=f"{os.path.splitext(coil_pos_fn)[0]}.xdmf")
|
|
1366
|
+
|
|
1367
|
+
return mats
|