pyNIBS 0.2024.8__py3-none-any.whl → 0.2026.1__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 (101) hide show
  1. pynibs/__init__.py +26 -14
  2. pynibs/coil/__init__.py +6 -0
  3. pynibs/{coil.py → coil/coil.py} +213 -543
  4. pynibs/coil/export.py +508 -0
  5. pynibs/congruence/__init__.py +4 -1
  6. pynibs/congruence/congruence.py +37 -45
  7. pynibs/congruence/ext_metrics.py +40 -11
  8. pynibs/congruence/stimulation_threshold.py +1 -2
  9. pynibs/expio/Mep.py +120 -370
  10. pynibs/expio/__init__.py +10 -0
  11. pynibs/expio/brainsight.py +34 -37
  12. pynibs/expio/cobot.py +25 -25
  13. pynibs/expio/exp.py +10 -7
  14. pynibs/expio/fit_funs.py +3 -0
  15. pynibs/expio/invesalius.py +70 -0
  16. pynibs/expio/localite.py +190 -91
  17. pynibs/expio/neurone.py +139 -0
  18. pynibs/expio/signal_ced.py +345 -2
  19. pynibs/expio/visor.py +16 -15
  20. pynibs/freesurfer.py +34 -33
  21. pynibs/hdf5_io/hdf5_io.py +149 -132
  22. pynibs/hdf5_io/xdmf.py +35 -31
  23. pynibs/mesh/__init__.py +1 -1
  24. pynibs/mesh/mesh_struct.py +77 -92
  25. pynibs/mesh/transformations.py +121 -21
  26. pynibs/mesh/utils.py +191 -99
  27. pynibs/models/_TMS.py +2 -1
  28. pynibs/muap.py +1 -2
  29. pynibs/neuron/__init__.py +10 -0
  30. pynibs/neuron/models/mep.py +566 -0
  31. pynibs/neuron/neuron_regression.py +98 -8
  32. pynibs/optimization/__init__.py +12 -2
  33. pynibs/optimization/{optimization.py → coil_opt.py} +157 -133
  34. pynibs/optimization/multichannel.py +1174 -24
  35. pynibs/optimization/workhorses.py +7 -8
  36. pynibs/regression/__init__.py +4 -2
  37. pynibs/regression/dual_node_detection.py +229 -219
  38. pynibs/regression/regression.py +92 -61
  39. pynibs/roi/__init__.py +4 -1
  40. pynibs/roi/roi_structs.py +19 -21
  41. pynibs/roi/{roi.py → roi_utils.py} +56 -33
  42. pynibs/subject.py +24 -14
  43. pynibs/util/__init__.py +20 -4
  44. pynibs/util/dosing.py +4 -5
  45. pynibs/util/quality_measures.py +39 -38
  46. pynibs/util/rotations.py +116 -9
  47. pynibs/util/{simnibs.py → simnibs_io.py} +29 -19
  48. pynibs/util/{util.py → utils.py} +20 -22
  49. pynibs/visualization/para.py +4 -4
  50. pynibs/visualization/render_3D.py +4 -4
  51. pynibs-0.2026.1.dist-info/METADATA +105 -0
  52. pynibs-0.2026.1.dist-info/RECORD +69 -0
  53. {pyNIBS-0.2024.8.dist-info → pynibs-0.2026.1.dist-info}/WHEEL +1 -1
  54. pyNIBS-0.2024.8.dist-info/METADATA +0 -723
  55. pyNIBS-0.2024.8.dist-info/RECORD +0 -107
  56. pynibs/data/configuration_exp0.yaml +0 -59
  57. pynibs/data/configuration_linear_MEP.yaml +0 -61
  58. pynibs/data/configuration_linear_RT.yaml +0 -61
  59. pynibs/data/configuration_sigmoid4.yaml +0 -68
  60. pynibs/data/network mapping configuration/configuration guide.md +0 -238
  61. pynibs/data/network mapping configuration/configuration_TEMPLATE.yaml +0 -42
  62. pynibs/data/network mapping configuration/configuration_for_testing.yaml +0 -43
  63. pynibs/data/network mapping configuration/configuration_modelTMS.yaml +0 -43
  64. pynibs/data/network mapping configuration/configuration_reg_isi_05.yaml +0 -43
  65. pynibs/data/network mapping configuration/output_documentation.md +0 -185
  66. pynibs/data/network mapping configuration/recommendations_for_accuracy_threshold.md +0 -77
  67. pynibs/data/neuron/models/L23_PC_cADpyr_biphasic_v1.csv +0 -1281
  68. pynibs/data/neuron/models/L23_PC_cADpyr_monophasic_v1.csv +0 -1281
  69. pynibs/data/neuron/models/L4_LBC_biphasic_v1.csv +0 -1281
  70. pynibs/data/neuron/models/L4_LBC_monophasic_v1.csv +0 -1281
  71. pynibs/data/neuron/models/L4_NBC_biphasic_v1.csv +0 -1281
  72. pynibs/data/neuron/models/L4_NBC_monophasic_v1.csv +0 -1281
  73. pynibs/data/neuron/models/L4_SBC_biphasic_v1.csv +0 -1281
  74. pynibs/data/neuron/models/L4_SBC_monophasic_v1.csv +0 -1281
  75. pynibs/data/neuron/models/L5_TTPC2_cADpyr_biphasic_v1.csv +0 -1281
  76. pynibs/data/neuron/models/L5_TTPC2_cADpyr_monophasic_v1.csv +0 -1281
  77. pynibs/tests/data/InstrumentMarker20200225163611937.xml +0 -19
  78. pynibs/tests/data/TriggerMarkers_Coil0_20200225163443682.xml +0 -14
  79. pynibs/tests/data/TriggerMarkers_Coil1_20200225170337572.xml +0 -6373
  80. pynibs/tests/data/Xdmf.dtd +0 -89
  81. pynibs/tests/data/brainsight_niiImage_nifticoord.txt +0 -145
  82. pynibs/tests/data/brainsight_niiImage_nifticoord_largefile.txt +0 -1434
  83. pynibs/tests/data/brainsight_niiImage_niifticoord_mixedtargets.txt +0 -47
  84. pynibs/tests/data/create_subject_testsub.py +0 -332
  85. pynibs/tests/data/data.hdf5 +0 -0
  86. pynibs/tests/data/geo.hdf5 +0 -0
  87. pynibs/tests/test_coil.py +0 -474
  88. pynibs/tests/test_elements2nodes.py +0 -100
  89. pynibs/tests/test_hdf5_io/test_xdmf.py +0 -61
  90. pynibs/tests/test_mesh_transformations.py +0 -123
  91. pynibs/tests/test_mesh_utils.py +0 -143
  92. pynibs/tests/test_nnav_imports.py +0 -101
  93. pynibs/tests/test_quality_measures.py +0 -117
  94. pynibs/tests/test_regressdata.py +0 -289
  95. pynibs/tests/test_roi.py +0 -17
  96. pynibs/tests/test_rotations.py +0 -86
  97. pynibs/tests/test_subject.py +0 -71
  98. pynibs/tests/test_util.py +0 -24
  99. /pynibs/{regression/score_types.py → neuron/models/m1_montbrio.py} +0 -0
  100. {pyNIBS-0.2024.8.dist-info → pynibs-0.2026.1.dist-info/licenses}/LICENSE +0 -0
  101. {pyNIBS-0.2024.8.dist-info → pynibs-0.2026.1.dist-info}/top_level.txt +0 -0
pynibs/coil/export.py ADDED
@@ -0,0 +1,508 @@
1
+ import os
2
+ import h5py
3
+ import numpy as np
4
+ import pandas as pd
5
+ import pynibs
6
+
7
+
8
+ def create_stimsite_hdf5(fn_exp, fn_hdf, conditions_selected=None, sep="_", merge_sites=False, fix_angles=False,
9
+ data_dict=None, conditions_ignored=None):
10
+ """
11
+ Reads results_conditions and creates an hdf5/xdmf pair with condition-wise centers of stimulation sites and
12
+ coil directions as data.
13
+
14
+ Parameters
15
+ ----------
16
+ fn_exp : str
17
+ Path to results.csv.
18
+ fn_hdf : str
19
+ Path where to write file. Gets overridden if already existing.
20
+ conditions_selected : str or list of str, optional
21
+ List of conditions returned by the function, the others are omitted.
22
+ If None, all conditions are returned.
23
+ sep: str, default: "_"
24
+ Separator between condition label and angle (e.g. M1_0, or M1-0).
25
+ merge_sites : bool
26
+ If true, only one coil center per site is generated.
27
+ fix_angles : bool
28
+ rename 22.5 -> 0, 0 -> -45, 67.5 -> 90, 90 -> 135.
29
+ data_dict : dict ofnp.ndarray of float [n_stimsites] (optional), default: None
30
+ Dictionary containing data corresponding to the stimulation sites (keys).
31
+ conditions_ignored : str or list of str, optional
32
+ Conditions, which are not going to be included in the plot.
33
+
34
+ Returns
35
+ -------
36
+ <Files> : hdf5/xdmf file pair
37
+ Contains information about condition-wise stimulation sites and coil directions (fn_hdf)
38
+
39
+ Example
40
+ -------
41
+ .. code-block:: python
42
+
43
+ pynibs.create_stimsite_hdf5('/exp/1/experiment_corrected.csv',
44
+ '/stimsite', True, True)
45
+ """
46
+ assert not fn_hdf.endswith('/')
47
+
48
+ exp = pynibs.expio.read_csv(fn_exp)
49
+
50
+ exp_cond = pynibs.expio.sort_by_condition(exp, conditions_selected=conditions_selected) # []
51
+
52
+ # get the unique conditions in the correct order
53
+ conds = [c['condition'][0] for c in exp_cond]
54
+
55
+ # remove conds
56
+ conds_temp = []
57
+ exp_cond_temp = []
58
+
59
+ if type(conditions_ignored) is not list:
60
+ conditions_ignored = [conditions_ignored]
61
+
62
+ for i_c, c in enumerate(conds):
63
+ ignore = False
64
+ for ci in list(conditions_ignored):
65
+ if c == ci:
66
+ ignore = True
67
+
68
+ if not ignore:
69
+ conds_temp.append(conds[i_c])
70
+ exp_cond_temp.append(exp_cond[i_c])
71
+
72
+ exp_cond = exp_cond_temp
73
+ conds = conds_temp
74
+
75
+ # hardcoded row #3 is condition
76
+ cond_idx = np.linspace(0, len(exp_cond), 1)[:, np.newaxis]
77
+
78
+ centers = []
79
+ m0 = []
80
+ m1 = []
81
+ m2 = []
82
+
83
+ for i_cond in range(len(exp_cond)):
84
+ centers.append(exp_cond[i_cond]['coil_mean_matrix'][0][0:3, 3])
85
+ m0.append(exp_cond[i_cond]['coil_mean_matrix'][0][0:3, 0])
86
+ m1.append(exp_cond[i_cond]['coil_mean_matrix'][0][0:3, 1])
87
+ m2.append(exp_cond[i_cond]['coil_mean_matrix'][0][0:3, 2])
88
+
89
+ # split conds to angles and sites: M1_90 -> M1, 90
90
+ angles = np.array([sp.split(sep)[-1] for sp in conds]).astype(np.float64)
91
+ sites = np.array([sp.split(sep)[0] for sp in conds])
92
+ sites_unique = np.unique(sites)
93
+
94
+ # average the center positions of a stimulation site over all orientations
95
+ if merge_sites:
96
+
97
+ # generate sites dict
98
+ centers_sites = dict()
99
+
100
+ for site in sites_unique:
101
+ centers_sites[site] = []
102
+
103
+ # gather all orientations and put them to the corresponding sites
104
+ for i_cond, site in enumerate(sites):
105
+ centers_sites[site].append(exp_cond[i_cond]['coil_mean_matrix'][0][0:3, 3])
106
+
107
+ # determine average position over all orientations for each site
108
+ for site in sites_unique:
109
+ centers_sites[site] = np.mean(np.vstack(centers_sites[site]), axis=0)
110
+
111
+ # write it back to centers
112
+ for i_cond, site in enumerate(sites):
113
+ centers[i_cond] = centers_sites[site]
114
+
115
+ centers = np.vstack(centers)
116
+ m0 = np.vstack(m0)
117
+ m1 = np.vstack(m1)
118
+ m2 = np.vstack(m2)
119
+
120
+ # enumerate sites, as paraview does not plot string array data
121
+ sites_idx = np.array(list(range(len(sites))))[:, np.newaxis]
122
+
123
+ angles[angles[:] == 675.] = 67.5
124
+ angles[angles[:] == 225.] = 22.5
125
+
126
+ if fix_angles:
127
+ # rename wrong angle names
128
+ angles_cor = np.copy(angles)
129
+ angles_cor[angles == 0] = -45.
130
+ angles_cor[angles == 22.5] = 0.
131
+ angles_cor[angles == 67.5] = 90.
132
+ angles_cor[angles == 90] = 135.
133
+ angles = angles_cor
134
+
135
+ # write hdf5 file
136
+ if not fn_hdf.endswith('.hdf5'):
137
+ fn_hdf += '.hdf5'
138
+ f = h5py.File(fn_hdf, 'w')
139
+ f.create_dataset('centers', data=centers.astype(np.float64))
140
+ f.create_dataset('m0', data=m0.astype(np.float64))
141
+ f.create_dataset('m1', data=m1.astype(np.float64))
142
+ f.create_dataset('m2', data=m2.astype(np.float64))
143
+ f.create_dataset('cond', data=np.string_(conds)) # this is a string array, not xdmf compatible
144
+ f.create_dataset('cond_idx', data=cond_idx)
145
+ f.create_dataset('angles', data=angles)
146
+ f.create_dataset('sites', data=np.string_(sites)) # this is a string array, not xdmf compatible
147
+ f.create_dataset('sites_idx', data=sites_idx)
148
+
149
+ data = None
150
+ if data_dict is not None:
151
+ data = np.zeros((len(list(data_dict.keys())), 1))
152
+ for i_data, cond in enumerate(conds):
153
+ data[i_data, 0] = data_dict[cond]
154
+ f.create_dataset('data', data=data)
155
+ f.close()
156
+
157
+ # write .xdmf file
158
+ f = open(fn_hdf[:-4] + 'xdmf', 'w')
159
+ fn_hdf = os.path.basename(fn_hdf) # relative links
160
+
161
+ # header
162
+ f.write('<?xml version="1.0"?>\n')
163
+ f.write('<!DOCTYPE Xdmf SYSTEM "Xdmf.dtd" []>\n')
164
+ f.write('<Xdmf Version="2.0" xmlns:xi="http://www.w3.org/2001/XInclude">\n')
165
+ f.write('<Domain>\n')
166
+ f.write('<Grid\nCollectionType="Spatial"\nGridType="Collection"\nName="Collection">\n')
167
+
168
+ # one grid for coil dipole nodes...store data hdf5.
169
+ #######################################################
170
+ f.write('<Grid Name="stimsites" GridType="Uniform">\n')
171
+ f.write('<Topology NumberOfElements="' + str(centers.shape[0]) +
172
+ '" TopologyType="Polyvertex" Name="Tri">\n')
173
+ f.write('<DataItem Format="XML" Dimensions="' + str(centers.shape[0]) + ' 1">\n')
174
+ # f.write(hdf5_fn + ':' + path + '/triangle_number_list\n')
175
+ np.savetxt(f, list(range(centers.shape[0])), fmt='%d', delimiter=' ') # 1 2 3 4 ... N_Points
176
+ f.write('</DataItem>\n')
177
+ f.write('</Topology>\n')
178
+
179
+ # nodes
180
+ f.write('<Geometry GeometryType="XYZ">\n')
181
+ f.write('<DataItem Format="HDF" Dimensions="' + str(centers.shape[0]) + ' 3">\n')
182
+ f.write(fn_hdf + ':' + '/centers\n')
183
+ f.write('</DataItem>\n')
184
+ f.write('</Geometry>\n')
185
+
186
+ # data
187
+ # dipole magnitude
188
+ # the 4 vectors
189
+ for i in range(3):
190
+ f.write('<Attribute Name="dir_' + str(i) + '" AttributeType="Vector" Center="Cell">\n')
191
+ f.write('<DataItem Format="HDF" Dimensions="' + str(centers.shape[0]) + ' 3">\n')
192
+ f.write(fn_hdf + ':' + '/m' + str(i) + '\n')
193
+ f.write('</DataItem>\n')
194
+ f.write('</Attribute>\n\n')
195
+
196
+ # angles
197
+ f.write('<Attribute Name="angles" AttributeType="Scalar" Center="Cell">\n')
198
+ f.write('<DataItem Format="HDF" Dimensions="' + str(centers.shape[0]) + ' 1">\n')
199
+ f.write(fn_hdf + ':' + '/angles\n')
200
+ f.write('</DataItem>\n')
201
+ f.write('</Attribute>\n\n')
202
+
203
+ # data
204
+ if data_dict is not None:
205
+ f.write('<Attribute Name="data" AttributeType="Scalar" Center="Cell">\n')
206
+ f.write('<DataItem Format="HDF" Dimensions="' + str(data.shape[0]) + ' 1">\n')
207
+ f.write(fn_hdf + ':' + '/data\n')
208
+ f.write('</DataItem>\n')
209
+ f.write('</Attribute>\n\n')
210
+
211
+ # site idx
212
+ f.write('<Attribute Name="sites_idx" AttributeType="Scalar" Center="Cell">\n')
213
+ f.write('<DataItem Format="HDF" Dimensions="' + str(centers.shape[0]) + ' 1">\n')
214
+ f.write(fn_hdf + ':' + '/sites_idx\n')
215
+ f.write('</DataItem>\n')
216
+ f.write('</Attribute>\n\n')
217
+
218
+ f.write('</Grid>\n')
219
+ # end coil dipole data
220
+
221
+ # footer
222
+ f.write('</Grid>\n')
223
+ f.write('</Domain>\n')
224
+ f.write('</Xdmf>\n')
225
+ f.close()
226
+
227
+
228
+ def create_stimsite_from_list(fn_hdf, poslist, datanames=None, data=None, overwrite=False):
229
+ """
230
+ This takes a list of matsimnibs-style coil position and orientations and creates an .hdf5 + .xdmf tuple
231
+ for all positions.
232
+
233
+ Centers and coil orientations are written to disk, with optional data for each coil configuration.
234
+
235
+ Parameters
236
+ ----------
237
+ fn_hdf: str
238
+ Filename for the .hdf5 file. The .xdmf is saved with the same basename.
239
+ Folder should already exist.
240
+ poslist: list of np.ndarray
241
+ (4,4) Positions.
242
+ datanames: str or list of str, optional
243
+ Dataset names for ``data``.
244
+ data: np.ndarray, optional
245
+ Dataset array with shape = ``(len(poslist.pos), len(datanames())``.
246
+ overwrite : bool, defaul: False
247
+ Overwrite existing files.
248
+ """
249
+ centers = []
250
+ m0 = []
251
+ m1 = []
252
+ m2 = []
253
+ if data is not None:
254
+ assert isinstance(data, np.ndarray)
255
+
256
+ for lst in poslist:
257
+ centers.append(lst[0:3, 3])
258
+ m0.append(lst[0:3, 0])
259
+ m1.append(lst[0:3, 1])
260
+ m2.append(lst[0:3, 2])
261
+
262
+ centers = np.vstack(centers)
263
+ m0 = np.vstack(m0)
264
+ m1 = np.vstack(m1)
265
+ m2 = np.vstack(m2)
266
+
267
+ write_coil_pos_hdf5(fn_hdf, centers, m0, m1, m2, datanames=datanames, data=data, overwrite=overwrite)
268
+
269
+
270
+ def create_stimsite_from_tmslist(fn_hdf, poslist, datanames=None, data=None, overwrite=False):
271
+ """
272
+ This takes a :py:class:simnibs.sim_struct.TMSLIST from simnibs and creates an .hdf5 + .xdmf tuple for all positions.
273
+
274
+ Centers and coil orientations are written to disk, with optional data for each coil configuration.
275
+
276
+ Parameters
277
+ ----------
278
+ fn_hdf: str
279
+ Filename for the .hdf5 file. The .xdmf is saved with the same basename.
280
+ Folder should already exist.
281
+ poslist: simnibs.sim_struct.TMSLIST
282
+ poslist.pos[*].matsimnibs have to be set.
283
+ datanames: str or list of str, optional
284
+ Dataset names for ``data``.
285
+ data: np.ndarray, optional
286
+ Dataset array with shape = ``(len(poslist.pos), len(datanames())``.
287
+ overwrite : bool, default: False
288
+ Overwrite existing files
289
+ """
290
+ centers = []
291
+ m0 = []
292
+ m1 = []
293
+ m2 = []
294
+ assert poslist.pos
295
+ if data is not None:
296
+ assert isinstance(data, np.ndarray)
297
+ for pos in poslist.pos:
298
+ assert pos.matsimnibs is not None
299
+ pos.matsimnibs = np.array(pos.matsimnibs)
300
+ centers.append(pos.matsimnibs[0:3, 3])
301
+ m0.append(pos.matsimnibs[0:3, 0])
302
+ m1.append(pos.matsimnibs[0:3, 1])
303
+ m2.append(pos.matsimnibs[0:3, 2])
304
+
305
+ centers = np.vstack(centers)
306
+ m0 = np.vstack(m0)
307
+ m1 = np.vstack(m1)
308
+ m2 = np.vstack(m2)
309
+
310
+ write_coil_pos_hdf5(fn_hdf, centers, m0, m1, m2, datanames=datanames, data=data, overwrite=overwrite)
311
+
312
+
313
+ def create_stimsite_from_exp_hdf5(fn_exp, fn_hdf, datanames=None, data=None, overwrite=False):
314
+ """
315
+ This takes an experiment.hdf5 file and creates an .hdf5 + .xdmf tuple for all coil positions for visualization.
316
+
317
+ Parameters
318
+ ----------
319
+ fn_exp : str
320
+ Path to experiment.hdf5
321
+ fn_hdf : str
322
+ Filename for the resulting .hdf5 file. The .xdmf is saved with the same basename.
323
+ Folder should already exist.
324
+ datanames : str or list of str, optional
325
+ Dataset names for ``data``
326
+ data : np.ndarray, optional
327
+ Dataset array with shape = ``(len(poslist.pos), len(datanames())``.
328
+ overwrite : bool, default: False
329
+ Overwrite existing files.
330
+ """
331
+ df_stim = pd.read_hdf(fn_exp, "stim_data")
332
+
333
+ matsimnibs = np.zeros((4, 4, df_stim.shape[0]))
334
+
335
+ for i in range(df_stim.shape[0]):
336
+ matsimnibs[:, :, i] = df_stim["coil_mean"].iloc[i]
337
+
338
+ create_stimsite_from_matsimnibs(fn_hdf=fn_hdf,
339
+ matsimnibs=matsimnibs,
340
+ datanames=datanames,
341
+ data=data,
342
+ overwrite=overwrite)
343
+
344
+
345
+ def create_stimsite_from_matsimnibs(fn_hdf, matsimnibs, datanames=None, data=None, overwrite=False):
346
+ """
347
+ This takes a matsimnibs array and creates an .hdf5 + .xdmf tuple for all coil positions for visualization.
348
+
349
+ Centers and coil orientations are written disk.
350
+
351
+ Parameters
352
+ ----------
353
+ fn_hdf: str
354
+ Filename for the .hdf5 file. The .xdmf is saved with the same basename.
355
+ Folder should already exist.
356
+ matsimnibs: np.ndarray
357
+ (4, 4, n_pos)
358
+ Matsimnibs matrices containing the coil orientation (x,y,z) and position (p)
359
+
360
+ .. math::
361
+ \\begin{bmatrix}
362
+ | & | & | & | \\\\
363
+ x & y & z & p \\\\
364
+ | & | & | & | \\\\
365
+ 0 & 0 & 0 & 1 \\\\
366
+ \\end{bmatrix}
367
+ datanames: str or list of str, optional
368
+ Dataset names for ``data``.
369
+ data: np.ndarray, optional
370
+ (len(poslist.pos), len(datanames).
371
+ overwrite : bool, default: False
372
+ Overwrite existing files.
373
+ """
374
+ matsimnibs = np.atleast_3d(matsimnibs)
375
+ n_pos = matsimnibs.shape[2]
376
+ centers = np.zeros((n_pos, 3))
377
+ m0 = np.zeros((n_pos, 3))
378
+ m1 = np.zeros((n_pos, 3))
379
+ m2 = np.zeros((n_pos, 3))
380
+ if data is not None:
381
+ assert isinstance(data, np.ndarray)
382
+
383
+ for i in range(matsimnibs.shape[2]):
384
+ centers[i, :] = matsimnibs[0:3, 3, i]
385
+ m0[i, :] = matsimnibs[0:3, 0, i]
386
+ m1[i, :] = matsimnibs[0:3, 1, i]
387
+ m2[i, :] = matsimnibs[0:3, 2, i]
388
+
389
+ write_coil_pos_hdf5(fn_hdf, centers, m0, m1, m2, datanames=datanames, data=data, overwrite=overwrite)
390
+
391
+
392
+ def write_coil_pos_hdf5(fn_hdf, centers, m0, m1, m2, datanames=None, data=None, overwrite=False):
393
+ """
394
+ Creates a ``.hdf5`` + ``.xdmf`` file tuple for all coil positions.
395
+ Coil centers and coil orientations are saved, and - optionally - data for each position if ``data`` and
396
+ ``datanames`` are provided.
397
+
398
+ Parameters
399
+ ----------
400
+ fn_hdf : str
401
+ Filename for the .hdf5 file. The .xdmf is saved with the same basename.
402
+ Folder should already exist.
403
+ centers : np.ndarray of float
404
+ (n_pos, 3) Coil positions.
405
+ m0 : np.ndarray of float
406
+ (n_pos, 3) Coil orientation x-axis (looking at the active (patient) side of the coil pointing to the right).
407
+ m1 : np.ndarray of float
408
+ (n_pos, 3) Coil orientation y-axis (looking at the active side of the coil pointing up away from the handle).
409
+ m2 : np.ndarray of float
410
+ (n_pos, 3) Coil orientation z-axis (looking at the active (patient) side of the coil pointing to the patient).
411
+ datanames : str or list of str, optional
412
+ (n_data) Dataset names for ``data``
413
+ data : np.ndarray, optional
414
+ (n_pos, n_data) Dataset array with (len(poslist.pos), len(datanames()).
415
+ overwrite : bool, default: False
416
+ Overwrite existing files.
417
+ """
418
+ n_pos = centers.shape[0]
419
+ if isinstance(datanames, str):
420
+ datanames = [datanames]
421
+
422
+ if data is not None:
423
+ if datanames is None:
424
+ raise ValueError("Provide datanames= with data= argument.")
425
+ if isinstance(datanames, str):
426
+ datanames = [datanames]
427
+ if len(data.shape) <= 1:
428
+ data = np.atleast_1d(data)[:, np.newaxis]
429
+ assert data.shape == (n_pos, len(datanames))
430
+ if datanames is not None and data is None:
431
+ raise ValueError("Provide data= with datanames= argument.")
432
+
433
+ m0_reshaped = np.hstack((m0, np.zeros((n_pos, 1)))).T[:, np.newaxis, :]
434
+ m1_reshaped = np.hstack((m1, np.zeros((n_pos, 1)))).T[:, np.newaxis, :]
435
+ m2_reshaped = np.hstack((m2, np.zeros((n_pos, 1)))).T[:, np.newaxis, :]
436
+ centers_reshaped = np.hstack((centers, np.ones((n_pos, 1)))).T[:, np.newaxis, :]
437
+
438
+ matsimnibs = np.concatenate((m0_reshaped, m1_reshaped, m2_reshaped, centers_reshaped), axis=1)
439
+
440
+ # write hdf5 file
441
+ if not fn_hdf.endswith('.hdf5'):
442
+ fn_hdf += '.hdf5'
443
+ if os.path.exists(fn_hdf) and not overwrite:
444
+ raise OSError(fn_hdf + " already exists. Set overwrite flag for create_stimsite_from_poslist.")
445
+
446
+ with h5py.File(fn_hdf, 'w') as f:
447
+ f.create_dataset('centers', data=centers.astype(np.float64))
448
+ f.create_dataset('m0', data=m0.astype(np.float64))
449
+ f.create_dataset('m1', data=m1.astype(np.float64))
450
+ f.create_dataset('m2', data=m2.astype(np.float64))
451
+ f.create_dataset("matsimnibs", data=matsimnibs)
452
+
453
+ if data is not None:
454
+ for i, col in enumerate(data.T):
455
+ f.create_dataset('/data/' + datanames[i], data=col)
456
+
457
+ # write .xdmf file
458
+ with open(fn_hdf[:-4] + 'xdmf', 'w') as f:
459
+ fn_hdf = os.path.basename(fn_hdf) # relative links
460
+
461
+ # header
462
+ f.write('<?xml version="1.0"?>\n')
463
+ f.write('<!DOCTYPE Xdmf SYSTEM "Xdmf.dtd" []>\n')
464
+ f.write('<Xdmf Version="2.0" xmlns:xi="http://www.w3.org/2001/XInclude">\n')
465
+ f.write('<Domain>\n')
466
+ f.write('<Grid CollectionType="Spatial" GridType="Collection" Name="Collection">\n')
467
+
468
+ # one grid for coil dipole nodes...store data hdf5.
469
+ #######################################################
470
+ f.write('<Grid Name="stimsites" GridType="Uniform">\n')
471
+ f.write(f'<Topology NumberOfElements="{centers.shape[0]}" TopologyType="Polyvertex" Name="Tri">\n')
472
+ f.write(f'\t<DataItem Format="XML" Dimensions="{centers.shape[0]} 1">\n')
473
+ np.savetxt(f, list(range(centers.shape[0])), fmt='\t%d', delimiter=' ') # 1 2 3 4 ... N_Points
474
+ f.write('\t</DataItem>\n')
475
+ f.write('</Topology>\n\n')
476
+
477
+ # nodes
478
+ f.write('<Geometry GeometryType="XYZ">\n')
479
+ f.write(f'\t<DataItem Format="HDF" Dimensions="{centers.shape[0]} 3">\n')
480
+ f.write(f'\t{fn_hdf}:/centers\n')
481
+ f.write('\t</DataItem>\n')
482
+ f.write('</Geometry>\n\n')
483
+
484
+ # data
485
+ # dipole magnitude
486
+ # the 4 vectors
487
+ for i in range(3):
488
+ f.write(f'\t\t<Attribute Name="dir_{i}" AttributeType="Vector" Center="Cell">\n')
489
+ f.write(f'\t\t\t<DataItem Format="HDF" Dimensions="{centers.shape[0]} 3">\n')
490
+ f.write(f'\t\t\t{fn_hdf}:/m{i}\n')
491
+ f.write('\t\t\t</DataItem>\n')
492
+ f.write('\t\t</Attribute>\n\n')
493
+
494
+ if data is not None:
495
+ for i, col in enumerate(data.T):
496
+ f.write(f'\t\t<Attribute Name="{datanames[i]}" AttributeType="Scalar" Center="Cell">\n')
497
+ f.write('\t\t\t<DataItem Format="HDF" Dimensions="' + str(centers.shape[0]) + ' 1">\n')
498
+ f.write(f'\t\t\t{fn_hdf}:/data/{datanames[i]}\n')
499
+ f.write('\t\t\t</DataItem>\n')
500
+ f.write('\t\t</Attribute>\n\n')
501
+
502
+ f.write('</Grid>\n')
503
+ # end coil dipole data
504
+
505
+ # footer
506
+ f.write('</Grid>\n')
507
+ f.write('</Domain>\n')
508
+ f.write('</Xdmf>\n')
@@ -1,6 +1,6 @@
1
1
  """
2
2
  :py:class:`pynibs.congruence` holds the initial congruence factor implementation as described in Weise, Numssen, et al., 2020 [1]_.
3
- This code is mainly stored here for reproducibility reasons. The current approach (Numssen et al., 2021; [2]_) uses
3
+ This code is mainly stored here for reproducibility reasons. The current approach (Numssen et al., 2021; [2]_; Jing et al., 2024; [3]_) uses
4
4
  the :py:class:`pynibs.regression` methods.
5
5
 
6
6
  References
@@ -9,6 +9,9 @@ References
9
9
  A novel approach to localize cortical TMS effects. Neuroimage, 209, 116486.
10
10
  .. [2] Numssen, O., Zier, A. L., Thielscher, A., Hartwigsen, G., Knösche, T. R., & Weise, K. (2021).
11
11
  Efficient high-resolution TMS mapping of the human motor cortex by nonlinear regression. NeuroImage, 245, 118654.
12
+ .. [3] Jing, Y., Numssen, O., Weise, K., Kalloch, B., Buchberger, L., Haueisen, J., Hartwigsen, G., Knösche, T. (2023).
13
+ Modeling the Effects of Transcranial Magnetic Stimulation on Spatial Attention. *Physics in Medicine & Biology*.
14
+ doi: `10.1088/1361-6560/acff34 <https://doi.org/10.1088/1361-6560/acff34>`_
12
15
  """
13
16
  from .congruence import *
14
17
  from .ext_metrics import *