pyNIBS 0.2024.8__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (107) hide show
  1. pyNIBS-0.2024.8.dist-info/LICENSE +623 -0
  2. pyNIBS-0.2024.8.dist-info/METADATA +723 -0
  3. pyNIBS-0.2024.8.dist-info/RECORD +107 -0
  4. pyNIBS-0.2024.8.dist-info/WHEEL +5 -0
  5. pyNIBS-0.2024.8.dist-info/top_level.txt +1 -0
  6. pynibs/__init__.py +34 -0
  7. pynibs/coil.py +1367 -0
  8. pynibs/congruence/__init__.py +15 -0
  9. pynibs/congruence/congruence.py +1108 -0
  10. pynibs/congruence/ext_metrics.py +257 -0
  11. pynibs/congruence/stimulation_threshold.py +318 -0
  12. pynibs/data/configuration_exp0.yaml +59 -0
  13. pynibs/data/configuration_linear_MEP.yaml +61 -0
  14. pynibs/data/configuration_linear_RT.yaml +61 -0
  15. pynibs/data/configuration_sigmoid4.yaml +68 -0
  16. pynibs/data/network mapping configuration/configuration guide.md +238 -0
  17. pynibs/data/network mapping configuration/configuration_TEMPLATE.yaml +42 -0
  18. pynibs/data/network mapping configuration/configuration_for_testing.yaml +43 -0
  19. pynibs/data/network mapping configuration/configuration_modelTMS.yaml +43 -0
  20. pynibs/data/network mapping configuration/configuration_reg_isi_05.yaml +43 -0
  21. pynibs/data/network mapping configuration/output_documentation.md +185 -0
  22. pynibs/data/network mapping configuration/recommendations_for_accuracy_threshold.md +77 -0
  23. pynibs/data/neuron/models/L23_PC_cADpyr_biphasic_v1.csv +1281 -0
  24. pynibs/data/neuron/models/L23_PC_cADpyr_monophasic_v1.csv +1281 -0
  25. pynibs/data/neuron/models/L4_LBC_biphasic_v1.csv +1281 -0
  26. pynibs/data/neuron/models/L4_LBC_monophasic_v1.csv +1281 -0
  27. pynibs/data/neuron/models/L4_NBC_biphasic_v1.csv +1281 -0
  28. pynibs/data/neuron/models/L4_NBC_monophasic_v1.csv +1281 -0
  29. pynibs/data/neuron/models/L4_SBC_biphasic_v1.csv +1281 -0
  30. pynibs/data/neuron/models/L4_SBC_monophasic_v1.csv +1281 -0
  31. pynibs/data/neuron/models/L5_TTPC2_cADpyr_biphasic_v1.csv +1281 -0
  32. pynibs/data/neuron/models/L5_TTPC2_cADpyr_monophasic_v1.csv +1281 -0
  33. pynibs/expio/Mep.py +1518 -0
  34. pynibs/expio/__init__.py +8 -0
  35. pynibs/expio/brainsight.py +979 -0
  36. pynibs/expio/brainvis.py +71 -0
  37. pynibs/expio/cobot.py +239 -0
  38. pynibs/expio/exp.py +1876 -0
  39. pynibs/expio/fit_funs.py +287 -0
  40. pynibs/expio/localite.py +1987 -0
  41. pynibs/expio/signal_ced.py +51 -0
  42. pynibs/expio/visor.py +624 -0
  43. pynibs/freesurfer.py +502 -0
  44. pynibs/hdf5_io/__init__.py +10 -0
  45. pynibs/hdf5_io/hdf5_io.py +1857 -0
  46. pynibs/hdf5_io/xdmf.py +1542 -0
  47. pynibs/mesh/__init__.py +3 -0
  48. pynibs/mesh/mesh_struct.py +1394 -0
  49. pynibs/mesh/transformations.py +866 -0
  50. pynibs/mesh/utils.py +1103 -0
  51. pynibs/models/_TMS.py +211 -0
  52. pynibs/models/__init__.py +0 -0
  53. pynibs/muap.py +392 -0
  54. pynibs/neuron/__init__.py +2 -0
  55. pynibs/neuron/neuron_regression.py +284 -0
  56. pynibs/neuron/util.py +58 -0
  57. pynibs/optimization/__init__.py +5 -0
  58. pynibs/optimization/multichannel.py +278 -0
  59. pynibs/optimization/opt_mep.py +152 -0
  60. pynibs/optimization/optimization.py +1445 -0
  61. pynibs/optimization/workhorses.py +698 -0
  62. pynibs/pckg/__init__.py +0 -0
  63. pynibs/pckg/biosig/biosig4c++-1.9.5.src_fixed.tar.gz +0 -0
  64. pynibs/pckg/libeep/__init__.py +0 -0
  65. pynibs/pckg/libeep/pyeep.so +0 -0
  66. pynibs/regression/__init__.py +11 -0
  67. pynibs/regression/dual_node_detection.py +2375 -0
  68. pynibs/regression/regression.py +2984 -0
  69. pynibs/regression/score_types.py +0 -0
  70. pynibs/roi/__init__.py +2 -0
  71. pynibs/roi/roi.py +895 -0
  72. pynibs/roi/roi_structs.py +1233 -0
  73. pynibs/subject.py +1009 -0
  74. pynibs/tensor_scaling.py +144 -0
  75. pynibs/tests/data/InstrumentMarker20200225163611937.xml +19 -0
  76. pynibs/tests/data/TriggerMarkers_Coil0_20200225163443682.xml +14 -0
  77. pynibs/tests/data/TriggerMarkers_Coil1_20200225170337572.xml +6373 -0
  78. pynibs/tests/data/Xdmf.dtd +89 -0
  79. pynibs/tests/data/brainsight_niiImage_nifticoord.txt +145 -0
  80. pynibs/tests/data/brainsight_niiImage_nifticoord_largefile.txt +1434 -0
  81. pynibs/tests/data/brainsight_niiImage_niifticoord_mixedtargets.txt +47 -0
  82. pynibs/tests/data/create_subject_testsub.py +332 -0
  83. pynibs/tests/data/data.hdf5 +0 -0
  84. pynibs/tests/data/geo.hdf5 +0 -0
  85. pynibs/tests/test_coil.py +474 -0
  86. pynibs/tests/test_elements2nodes.py +100 -0
  87. pynibs/tests/test_hdf5_io/test_xdmf.py +61 -0
  88. pynibs/tests/test_mesh_transformations.py +123 -0
  89. pynibs/tests/test_mesh_utils.py +143 -0
  90. pynibs/tests/test_nnav_imports.py +101 -0
  91. pynibs/tests/test_quality_measures.py +117 -0
  92. pynibs/tests/test_regressdata.py +289 -0
  93. pynibs/tests/test_roi.py +17 -0
  94. pynibs/tests/test_rotations.py +86 -0
  95. pynibs/tests/test_subject.py +71 -0
  96. pynibs/tests/test_util.py +24 -0
  97. pynibs/tms_pulse.py +34 -0
  98. pynibs/util/__init__.py +4 -0
  99. pynibs/util/dosing.py +233 -0
  100. pynibs/util/quality_measures.py +562 -0
  101. pynibs/util/rotations.py +340 -0
  102. pynibs/util/simnibs.py +763 -0
  103. pynibs/util/util.py +727 -0
  104. pynibs/visualization/__init__.py +2 -0
  105. pynibs/visualization/para.py +4372 -0
  106. pynibs/visualization/plot_2D.py +137 -0
  107. pynibs/visualization/render_3D.py +347 -0
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