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.
- pynibs/__init__.py +26 -14
- pynibs/coil/__init__.py +6 -0
- pynibs/{coil.py → coil/coil.py} +213 -543
- pynibs/coil/export.py +508 -0
- pynibs/congruence/__init__.py +4 -1
- pynibs/congruence/congruence.py +37 -45
- pynibs/congruence/ext_metrics.py +40 -11
- pynibs/congruence/stimulation_threshold.py +1 -2
- pynibs/expio/Mep.py +120 -370
- pynibs/expio/__init__.py +10 -0
- pynibs/expio/brainsight.py +34 -37
- pynibs/expio/cobot.py +25 -25
- pynibs/expio/exp.py +10 -7
- pynibs/expio/fit_funs.py +3 -0
- pynibs/expio/invesalius.py +70 -0
- pynibs/expio/localite.py +190 -91
- pynibs/expio/neurone.py +139 -0
- pynibs/expio/signal_ced.py +345 -2
- pynibs/expio/visor.py +16 -15
- pynibs/freesurfer.py +34 -33
- pynibs/hdf5_io/hdf5_io.py +149 -132
- pynibs/hdf5_io/xdmf.py +35 -31
- pynibs/mesh/__init__.py +1 -1
- pynibs/mesh/mesh_struct.py +77 -92
- pynibs/mesh/transformations.py +121 -21
- pynibs/mesh/utils.py +191 -99
- pynibs/models/_TMS.py +2 -1
- pynibs/muap.py +1 -2
- pynibs/neuron/__init__.py +10 -0
- pynibs/neuron/models/mep.py +566 -0
- pynibs/neuron/neuron_regression.py +98 -8
- pynibs/optimization/__init__.py +12 -2
- pynibs/optimization/{optimization.py → coil_opt.py} +157 -133
- pynibs/optimization/multichannel.py +1174 -24
- pynibs/optimization/workhorses.py +7 -8
- pynibs/regression/__init__.py +4 -2
- pynibs/regression/dual_node_detection.py +229 -219
- pynibs/regression/regression.py +92 -61
- pynibs/roi/__init__.py +4 -1
- pynibs/roi/roi_structs.py +19 -21
- pynibs/roi/{roi.py → roi_utils.py} +56 -33
- pynibs/subject.py +24 -14
- pynibs/util/__init__.py +20 -4
- pynibs/util/dosing.py +4 -5
- pynibs/util/quality_measures.py +39 -38
- pynibs/util/rotations.py +116 -9
- pynibs/util/{simnibs.py → simnibs_io.py} +29 -19
- pynibs/util/{util.py → utils.py} +20 -22
- pynibs/visualization/para.py +4 -4
- pynibs/visualization/render_3D.py +4 -4
- pynibs-0.2026.1.dist-info/METADATA +105 -0
- pynibs-0.2026.1.dist-info/RECORD +69 -0
- {pyNIBS-0.2024.8.dist-info → pynibs-0.2026.1.dist-info}/WHEEL +1 -1
- pyNIBS-0.2024.8.dist-info/METADATA +0 -723
- pyNIBS-0.2024.8.dist-info/RECORD +0 -107
- pynibs/data/configuration_exp0.yaml +0 -59
- pynibs/data/configuration_linear_MEP.yaml +0 -61
- pynibs/data/configuration_linear_RT.yaml +0 -61
- pynibs/data/configuration_sigmoid4.yaml +0 -68
- pynibs/data/network mapping configuration/configuration guide.md +0 -238
- pynibs/data/network mapping configuration/configuration_TEMPLATE.yaml +0 -42
- pynibs/data/network mapping configuration/configuration_for_testing.yaml +0 -43
- pynibs/data/network mapping configuration/configuration_modelTMS.yaml +0 -43
- pynibs/data/network mapping configuration/configuration_reg_isi_05.yaml +0 -43
- pynibs/data/network mapping configuration/output_documentation.md +0 -185
- pynibs/data/network mapping configuration/recommendations_for_accuracy_threshold.md +0 -77
- pynibs/data/neuron/models/L23_PC_cADpyr_biphasic_v1.csv +0 -1281
- pynibs/data/neuron/models/L23_PC_cADpyr_monophasic_v1.csv +0 -1281
- pynibs/data/neuron/models/L4_LBC_biphasic_v1.csv +0 -1281
- pynibs/data/neuron/models/L4_LBC_monophasic_v1.csv +0 -1281
- pynibs/data/neuron/models/L4_NBC_biphasic_v1.csv +0 -1281
- pynibs/data/neuron/models/L4_NBC_monophasic_v1.csv +0 -1281
- pynibs/data/neuron/models/L4_SBC_biphasic_v1.csv +0 -1281
- pynibs/data/neuron/models/L4_SBC_monophasic_v1.csv +0 -1281
- pynibs/data/neuron/models/L5_TTPC2_cADpyr_biphasic_v1.csv +0 -1281
- pynibs/data/neuron/models/L5_TTPC2_cADpyr_monophasic_v1.csv +0 -1281
- pynibs/tests/data/InstrumentMarker20200225163611937.xml +0 -19
- pynibs/tests/data/TriggerMarkers_Coil0_20200225163443682.xml +0 -14
- pynibs/tests/data/TriggerMarkers_Coil1_20200225170337572.xml +0 -6373
- pynibs/tests/data/Xdmf.dtd +0 -89
- pynibs/tests/data/brainsight_niiImage_nifticoord.txt +0 -145
- pynibs/tests/data/brainsight_niiImage_nifticoord_largefile.txt +0 -1434
- pynibs/tests/data/brainsight_niiImage_niifticoord_mixedtargets.txt +0 -47
- pynibs/tests/data/create_subject_testsub.py +0 -332
- pynibs/tests/data/data.hdf5 +0 -0
- pynibs/tests/data/geo.hdf5 +0 -0
- pynibs/tests/test_coil.py +0 -474
- pynibs/tests/test_elements2nodes.py +0 -100
- pynibs/tests/test_hdf5_io/test_xdmf.py +0 -61
- pynibs/tests/test_mesh_transformations.py +0 -123
- pynibs/tests/test_mesh_utils.py +0 -143
- pynibs/tests/test_nnav_imports.py +0 -101
- pynibs/tests/test_quality_measures.py +0 -117
- pynibs/tests/test_regressdata.py +0 -289
- pynibs/tests/test_roi.py +0 -17
- pynibs/tests/test_rotations.py +0 -86
- pynibs/tests/test_subject.py +0 -71
- pynibs/tests/test_util.py +0 -24
- /pynibs/{regression/score_types.py → neuron/models/m1_montbrio.py} +0 -0
- {pyNIBS-0.2024.8.dist-info → pynibs-0.2026.1.dist-info/licenses}/LICENSE +0 -0
- {pyNIBS-0.2024.8.dist-info → pynibs-0.2026.1.dist-info}/top_level.txt +0 -0
pynibs/mesh/utils.py
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
|
+
import os
|
|
1
2
|
import h5py
|
|
2
3
|
import math
|
|
4
|
+
import meshio
|
|
5
|
+
import trimesh
|
|
3
6
|
import warnings
|
|
4
7
|
import numpy as np
|
|
5
8
|
import pandas as pd
|
|
@@ -8,7 +11,6 @@ import multiprocessing
|
|
|
8
11
|
from functools import partial
|
|
9
12
|
from numpy import cross as cycross
|
|
10
13
|
from scipy.spatial import Delaunay
|
|
11
|
-
|
|
12
14
|
import pynibs
|
|
13
15
|
|
|
14
16
|
|
|
@@ -38,12 +40,12 @@ def calc_tet_volume(points, abs=True):
|
|
|
38
40
|
Returns
|
|
39
41
|
-------
|
|
40
42
|
volume: np.ndarray
|
|
41
|
-
shape: ``(n_tets)
|
|
43
|
+
shape: ``(n_tets)``-
|
|
42
44
|
|
|
43
45
|
Other Parameters
|
|
44
46
|
----------------
|
|
45
47
|
abs : bool, default: true
|
|
46
|
-
Return magnitude
|
|
48
|
+
Return magnitude-
|
|
47
49
|
"""
|
|
48
50
|
if points.ndim == 2:
|
|
49
51
|
points = np.atleast_3d(points).reshape(1, 4, 3)
|
|
@@ -107,7 +109,7 @@ def get_sphere(mesh=None, mesh_fn=None, target=None, radius=None, roi_idx=None,
|
|
|
107
109
|
Returns
|
|
108
110
|
-------
|
|
109
111
|
elms_in_sphere : np.ndarray
|
|
110
|
-
(n_elements): Indices of elements found in ROI
|
|
112
|
+
(n_elements): Indices of elements found in ROI-
|
|
111
113
|
"""
|
|
112
114
|
# let's handle the input parameter combinations
|
|
113
115
|
assert target is not None
|
|
@@ -121,9 +123,9 @@ def get_sphere(mesh=None, mesh_fn=None, target=None, radius=None, roi_idx=None,
|
|
|
121
123
|
if mesh is not None:
|
|
122
124
|
raise ValueError("Either provide mesh or mesh_fn")
|
|
123
125
|
if mesh_fn.endswith('.hdf5'):
|
|
124
|
-
mesh = pynibs.load_mesh_hdf5(mesh_fn)
|
|
126
|
+
mesh = pynibs.hdf5_io.load_mesh_hdf5(mesh_fn)
|
|
125
127
|
elif mesh_fn.endswith('.msh'):
|
|
126
|
-
mesh = pynibs.load_mesh_msh(mesh_fn)
|
|
128
|
+
mesh = pynibs.hdf5_io.load_mesh_msh(mesh_fn)
|
|
127
129
|
|
|
128
130
|
if roi is None and roi_idx is not None:
|
|
129
131
|
if mesh_fn is None:
|
|
@@ -153,17 +155,16 @@ def tets_in_sphere(mesh, target, radius, roi, domain=None):
|
|
|
153
155
|
----------
|
|
154
156
|
mesh : pynibs.TetrahedraLinear, optional
|
|
155
157
|
target : np.ndarray of float, optional
|
|
156
|
-
(3,) X, Y, Z coordinates of target
|
|
158
|
+
(3,) X, Y, Z coordinates of target-
|
|
157
159
|
radius : float, optional
|
|
158
|
-
Sphere radius im mm
|
|
160
|
+
Sphere radius im mm-
|
|
159
161
|
roi : pynibs.mesh.ROI, optional
|
|
160
|
-
Region of interest
|
|
162
|
+
Region of interest-
|
|
161
163
|
|
|
162
164
|
Returns
|
|
163
165
|
-------
|
|
164
166
|
tets_in_sphere : np.ndarray
|
|
165
|
-
(n_tets): Indices of elements found in ROI
|
|
166
|
-
|
|
167
|
+
(n_tets): Indices of elements found in ROI-
|
|
167
168
|
"""
|
|
168
169
|
if roi is None:
|
|
169
170
|
if radius is None or radius == 0:
|
|
@@ -201,16 +202,16 @@ def tris_in_sphere(mesh, target, radius, roi):
|
|
|
201
202
|
----------
|
|
202
203
|
mesh : pynibs.mesh.TetrahedraLinear, optional
|
|
203
204
|
target : np.ndarray of float or list of float
|
|
204
|
-
(3,) X, Y, Z coordinates of target
|
|
205
|
+
(3,) X, Y, Z coordinates of target-
|
|
205
206
|
radius : float
|
|
206
207
|
Sphere radius im mm
|
|
207
208
|
roi : pynibs.mesh.mesh_struct.ROI, optional
|
|
208
|
-
ROI
|
|
209
|
+
ROI-
|
|
209
210
|
|
|
210
211
|
Returns
|
|
211
212
|
-------
|
|
212
213
|
tris_in_sphere : np.ndarray
|
|
213
|
-
(n_triangles): Indices of elements found in sphere
|
|
214
|
+
(n_triangles): Indices of elements found in sphere.
|
|
214
215
|
"""
|
|
215
216
|
if roi is None:
|
|
216
217
|
if radius is None or radius == 0:
|
|
@@ -244,7 +245,6 @@ def sample_sphere(n_points, r):
|
|
|
244
245
|
points: np.ndarray of float
|
|
245
246
|
(N x 3), Evenly spread points in a unit sphere.
|
|
246
247
|
"""
|
|
247
|
-
|
|
248
248
|
assert n_points % 2 == 1, "The number of points must be odd"
|
|
249
249
|
points = []
|
|
250
250
|
|
|
@@ -268,35 +268,34 @@ def sample_sphere(n_points, r):
|
|
|
268
268
|
def get_indices_discontinuous_data(data, con, neighbor=False, deviation_factor=2,
|
|
269
269
|
min_val=None, not_fitted_elms=None, crit='median', neigh_style='point'):
|
|
270
270
|
"""
|
|
271
|
-
Get element indices (and the best neighbor index), where the data is discontinuous
|
|
271
|
+
Get element indices (and the best neighbor index), where the data is discontinuous.
|
|
272
272
|
|
|
273
273
|
Parameters
|
|
274
274
|
----------
|
|
275
275
|
data : np.ndarray of float [n_data]
|
|
276
|
-
Data array to analyze given in the element center
|
|
276
|
+
Data array to analyze given in the element center.
|
|
277
277
|
con : np.ndarray of float [n_data, 3 or 4]
|
|
278
|
-
Connectivity matrix
|
|
278
|
+
Connectivity matrix.
|
|
279
279
|
neighbor : bool, default: False
|
|
280
|
-
Return also the element index of the "best" neighbor (w.r.t. median of data)
|
|
280
|
+
Return also the element index of the "best" neighbor (w.r.t. median of data).
|
|
281
281
|
deviation_factor : float
|
|
282
|
-
Allows data deviation from 1/deviation_factor < data[i]/median < deviation_factor
|
|
282
|
+
Allows data deviation from 1/deviation_factor < data[i]/median < deviation_factor.
|
|
283
283
|
min_val : float, optional
|
|
284
284
|
If given, only return elements which have a neighbor with data higher than min_val.
|
|
285
285
|
not_fitted_elms : np.ndarray
|
|
286
|
-
If given, these elements are not used as neighbors
|
|
286
|
+
If given, these elements are not used as neighbors.
|
|
287
287
|
crit: str, default: median
|
|
288
|
-
Criterium for best neighbor. Either median or max value
|
|
288
|
+
Criterium for best neighbor. Either median or max value.
|
|
289
289
|
neigh_style : str, default: 'point'
|
|
290
|
-
Should neighbors share point or 'edge'
|
|
290
|
+
Should neighbors share point or 'edge'.
|
|
291
291
|
|
|
292
292
|
Returns
|
|
293
293
|
-------
|
|
294
294
|
idx_disc : list of int [n_disc]
|
|
295
|
-
Index list containing the indices of the discontinuous elements
|
|
295
|
+
Index list containing the indices of the discontinuous elements.
|
|
296
296
|
idx_neighbor : list of int [n_disc]
|
|
297
|
-
Index list containing the indices of the "best" neighbors of the discontinuous elements
|
|
297
|
+
Index list containing the indices of the "best" neighbors of the discontinuous elements.
|
|
298
298
|
"""
|
|
299
|
-
|
|
300
299
|
n_ele = con.shape[0]
|
|
301
300
|
idx_disc, idx_neighbor = [], []
|
|
302
301
|
|
|
@@ -409,7 +408,6 @@ def find_nearest(array, value):
|
|
|
409
408
|
-------
|
|
410
409
|
idx : int
|
|
411
410
|
Index j such that "value" is between array[j] and array[j+1].
|
|
412
|
-
|
|
413
411
|
"""
|
|
414
412
|
n = len(array)
|
|
415
413
|
if value < array[0]:
|
|
@@ -451,10 +449,9 @@ def in_hull(points, hull):
|
|
|
451
449
|
Returns
|
|
452
450
|
-------
|
|
453
451
|
inside : np.ndarray of bool
|
|
454
|
-
TRUE: point inside the hull
|
|
455
|
-
FALSE: point outside the hull
|
|
452
|
+
TRUE: point inside the hull.
|
|
453
|
+
FALSE: point outside the hull.
|
|
456
454
|
"""
|
|
457
|
-
|
|
458
455
|
if not isinstance(hull, Delaunay):
|
|
459
456
|
hull = Delaunay(hull)
|
|
460
457
|
return hull.find_simplex(points) >= 0
|
|
@@ -477,20 +474,19 @@ def calc_tetrahedra_volume_cross(P1, P2, P3, P4):
|
|
|
477
474
|
Parameters
|
|
478
475
|
----------
|
|
479
476
|
P1 : np.ndarray of float [N_tet x 3]
|
|
480
|
-
Coordinates of first point of tetrahedra
|
|
477
|
+
Coordinates of first point of tetrahedra.
|
|
481
478
|
P2 : np.ndarray of float [N_tet x 3]
|
|
482
|
-
Coordinates of second point of tetrahedra
|
|
479
|
+
Coordinates of second point of tetrahedra.
|
|
483
480
|
P3 : np.ndarray of float [N_tet x 3]
|
|
484
|
-
Coordinates of third point of tetrahedra
|
|
481
|
+
Coordinates of third point of tetrahedra.
|
|
485
482
|
P4 : np.ndarray of float [N_tet x 3]
|
|
486
|
-
Coordinates of fourth point of tetrahedra
|
|
483
|
+
Coordinates of fourth point of tetrahedra.
|
|
487
484
|
|
|
488
485
|
Returns
|
|
489
486
|
-------
|
|
490
487
|
tetrahedra_volume: np.ndarray of float [N_tet x 1]
|
|
491
|
-
Volumes of tetrahedra
|
|
488
|
+
Volumes of tetrahedra.
|
|
492
489
|
"""
|
|
493
|
-
|
|
494
490
|
tetrahedra_volume = 1.0 / 6 * \
|
|
495
491
|
np.sum(np.multiply(cycross(P2 - P1, P3 - P1), P4 - P1), 1)
|
|
496
492
|
tetrahedra_volume = tetrahedra_volume[:, np.newaxis]
|
|
@@ -500,7 +496,7 @@ def calc_tetrahedra_volume_cross(P1, P2, P3, P4):
|
|
|
500
496
|
def calc_tetrahedra_volume_det(P1, P2, P3, P4):
|
|
501
497
|
"""
|
|
502
498
|
Calculate volume of tetrahedron specified by 4 points P1...P4
|
|
503
|
-
multiple tetrahedra can be defined by P1...P4 as 2-D np.
|
|
499
|
+
multiple tetrahedra can be defined by P1...P4 as 2-D np.ndarray
|
|
504
500
|
using the determinant.
|
|
505
501
|
|
|
506
502
|
|
|
@@ -515,20 +511,19 @@ def calc_tetrahedra_volume_det(P1, P2, P3, P4):
|
|
|
515
511
|
Parameters
|
|
516
512
|
----------
|
|
517
513
|
P1 : np.ndarray of float [N_tet x 3]
|
|
518
|
-
Coordinates of first point of tetrahedra
|
|
514
|
+
Coordinates of first point of tetrahedra.
|
|
519
515
|
P2 : np.ndarray of float [N_tet x 3]
|
|
520
|
-
Coordinates of second point of tetrahedra
|
|
516
|
+
Coordinates of second point of tetrahedra.
|
|
521
517
|
P3 : np.ndarray of float [N_tet x 3]
|
|
522
|
-
Coordinates of third point of tetrahedra
|
|
518
|
+
Coordinates of third point of tetrahedra.
|
|
523
519
|
P4 : np.ndarray of float [N_tet x 3]
|
|
524
|
-
Coordinates of fourth point of tetrahedra
|
|
520
|
+
Coordinates of fourth point of tetrahedra.
|
|
525
521
|
|
|
526
522
|
Returns
|
|
527
523
|
-------
|
|
528
524
|
tetrahedra_volume : np.ndarray of float [N_tet x 1]
|
|
529
|
-
Volumes of tetrahedra
|
|
525
|
+
Volumes of tetrahedra.
|
|
530
526
|
"""
|
|
531
|
-
|
|
532
527
|
N_tets = P1.shape[0] if P1.ndim > 1 else 1
|
|
533
528
|
|
|
534
529
|
# add ones
|
|
@@ -557,18 +552,17 @@ def calc_gradient_surface(phi, points, triangles):
|
|
|
557
552
|
Parameters
|
|
558
553
|
----------
|
|
559
554
|
phi : np.ndarray of float [N_points x 1]
|
|
560
|
-
Potential in nodes
|
|
555
|
+
Potential in nodes.
|
|
561
556
|
points : np.ndarray of float [N_points x 3]
|
|
562
|
-
Coordinates of nodes (x,y,z)
|
|
557
|
+
Coordinates of nodes (x,y,z).
|
|
563
558
|
triangles : np.ndarray of int32 [N_tri x 3]
|
|
564
|
-
Connectivity of triangular mesh
|
|
559
|
+
Connectivity of triangular mesh.
|
|
565
560
|
|
|
566
561
|
Returns
|
|
567
562
|
-------
|
|
568
563
|
grad_phi : np.ndarray of float [N_tri x 3]
|
|
569
|
-
Gradient of potential phi on surface
|
|
564
|
+
Gradient of potential phi on surface.
|
|
570
565
|
"""
|
|
571
|
-
|
|
572
566
|
grad_phi = np.zeros((triangles.shape[0], 3))
|
|
573
567
|
|
|
574
568
|
for i in range(triangles.shape[0]):
|
|
@@ -594,11 +588,10 @@ def determine_e_midlayer_workhorse(fn_e_results, subject, mesh_idx, midlayer_fun
|
|
|
594
588
|
simnibs < 3.0 : 1000.
|
|
595
589
|
simnibs >= 3.0 : 1. (Default)
|
|
596
590
|
"""
|
|
597
|
-
|
|
598
591
|
if verbose:
|
|
599
592
|
print(f"Loading Mesh and ROI {roi_idx} from {fn_mesh_hdf5}")
|
|
600
593
|
|
|
601
|
-
msh = pynibs.load_mesh_hdf5(fn_mesh_hdf5)
|
|
594
|
+
msh = pynibs.hdf5_io.load_mesh_hdf5(fn_mesh_hdf5)
|
|
602
595
|
roi = pynibs.load_roi_surface_obj_from_hdf5(fn_mesh_hdf5)
|
|
603
596
|
|
|
604
597
|
for fn_e in fn_e_results:
|
|
@@ -666,28 +659,27 @@ def determine_e_midlayer(fn_e_results, fn_mesh_hdf5, subject, mesh_idx, roi_idx,
|
|
|
666
659
|
Parameters
|
|
667
660
|
----------
|
|
668
661
|
fn_e_results : list of str
|
|
669
|
-
List of results filenames (.hdf5 format)
|
|
662
|
+
List of results filenames (.hdf5 format).
|
|
670
663
|
fn_mesh_hdf5 : str
|
|
671
|
-
Filename of corresponding mesh file
|
|
664
|
+
Filename of corresponding mesh file.
|
|
672
665
|
subject : pynibs.Subject
|
|
673
|
-
Subject object
|
|
666
|
+
Subject object.
|
|
674
667
|
mesh_idx : int
|
|
675
|
-
Mesh index
|
|
668
|
+
Mesh index.
|
|
676
669
|
roi_idx : int
|
|
677
|
-
ROI index
|
|
670
|
+
ROI index.
|
|
678
671
|
n_cpu : int, default: 4
|
|
679
|
-
Number of parallel computations
|
|
672
|
+
Number of parallel computations.
|
|
680
673
|
midlayer_fun : str, default: "simnibs"
|
|
681
|
-
Method to determine the midlayer e-fields ("pynibs" or "simnibs")
|
|
674
|
+
Method to determine the midlayer e-fields ("pynibs" or "simnibs").
|
|
682
675
|
phi_scaling : float, default: 1.0
|
|
683
|
-
Scaling factor of scalar potential to change between "m" and "mm"
|
|
676
|
+
Scaling factor of scalar potential to change between "m" and "mm".
|
|
684
677
|
|
|
685
678
|
Returns
|
|
686
679
|
-------
|
|
687
680
|
<File> .hdf5 file
|
|
688
|
-
Adds midlayer e-field results to ROI
|
|
681
|
+
Adds midlayer e-field results to ROI.
|
|
689
682
|
"""
|
|
690
|
-
|
|
691
683
|
# msh = pynibs.load_mesh_msh(subject.mesh[mesh_idx]['fn_mesh_msh'])
|
|
692
684
|
|
|
693
685
|
n_cpu_available = multiprocessing.cpu_count()
|
|
@@ -702,7 +694,7 @@ def determine_e_midlayer(fn_e_results, fn_mesh_hdf5, subject, mesh_idx, roi_idx,
|
|
|
702
694
|
phi_scaling=phi_scaling,
|
|
703
695
|
verbose=verbose)
|
|
704
696
|
|
|
705
|
-
fn_e_results_chunks = pynibs.compute_chunks(fn_e_results, n_cpu)
|
|
697
|
+
fn_e_results_chunks = pynibs.util.utils.compute_chunks(fn_e_results, n_cpu)
|
|
706
698
|
pool = multiprocessing.Pool(n_cpu)
|
|
707
699
|
pool.map(workhorse_partial, fn_e_results_chunks)
|
|
708
700
|
pool.close()
|
|
@@ -716,18 +708,17 @@ def find_element_idx_by_points(nodes, con, points):
|
|
|
716
708
|
Parameters
|
|
717
709
|
----------
|
|
718
710
|
nodes : np.ndarray [N_nodes x 3]
|
|
719
|
-
Coordinates (x, y, z) of the nodes
|
|
711
|
+
Coordinates (x, y, z) of the nodes.
|
|
720
712
|
con : np.ndarray [N_tet x 4]
|
|
721
|
-
Connectivity matrix
|
|
713
|
+
Connectivity matrix.
|
|
722
714
|
points : np.ndarray [N_points x 3]
|
|
723
715
|
Points for which the element indices are found.
|
|
724
716
|
|
|
725
717
|
Returns
|
|
726
718
|
-------
|
|
727
719
|
ele_idx : np.ndarray [N_points]
|
|
728
|
-
Element indices of tetrahedra where corresponding 'points' are lying in
|
|
720
|
+
Element indices of tetrahedra where corresponding 'points' are lying in.
|
|
729
721
|
"""
|
|
730
|
-
|
|
731
722
|
node_idx = []
|
|
732
723
|
for i in range(points.shape[0]):
|
|
733
724
|
node_idx.append(np.where(np.linalg.norm(nodes - points[i, :], axis=1) < 1e-2)[0])
|
|
@@ -748,6 +739,7 @@ def check_islands_for_single_elm(source_elm, connectivity=None, adjacency=None,
|
|
|
748
739
|
3. Continue recursively with all 2-node-neighbors and visit their 2-node-neighbors
|
|
749
740
|
4. See if any 1-node-neighbors have not been visited with this strategy. If so, an island has been found
|
|
750
741
|
|
|
742
|
+
|
|
751
743
|
Parameters
|
|
752
744
|
----------
|
|
753
745
|
source_elm : int
|
|
@@ -852,15 +844,16 @@ def find_islands(connectivity=None, adjacency=None, island_crit='any', verbose=F
|
|
|
852
844
|
largest : book, default: False
|
|
853
845
|
Only return largest island, speeds up computation quite a bit if only one large, and many small islands exist.
|
|
854
846
|
verbose : bool, optional
|
|
855
|
-
Print some verbosity information. Default: False
|
|
847
|
+
Print some verbosity information. Default: False.
|
|
848
|
+
|
|
856
849
|
Returns
|
|
857
850
|
-------
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
851
|
+
elms_with_island : list
|
|
852
|
+
Elements with neighboring islands.
|
|
853
|
+
counter_visited : np.ndarray
|
|
854
|
+
shape = (n_elms). How often as each element been visited.
|
|
855
|
+
counter_not_visited : np.ndarray
|
|
856
|
+
shape = (n_elms). How often as each element not been visited.
|
|
864
857
|
"""
|
|
865
858
|
elms_with_island = []
|
|
866
859
|
if adjacency is not None and connectivity is not None:
|
|
@@ -914,7 +907,7 @@ def find_islands(connectivity=None, adjacency=None, island_crit='any', verbose=F
|
|
|
914
907
|
def find_island_elms(connectivity=None, adjacency=None, verbose=False, island_crit='edge', decision='cumulative'):
|
|
915
908
|
"""
|
|
916
909
|
Searches for islands in a mesh and returns element indices of the smallest island.
|
|
917
|
-
Island is
|
|
910
|
+
Island is defind as a set of elements, which share a single node and/or single edge with the rest of the mesh.
|
|
918
911
|
|
|
919
912
|
Parameters
|
|
920
913
|
----------
|
|
@@ -936,7 +929,7 @@ def find_island_elms(connectivity=None, adjacency=None, verbose=False, island_cr
|
|
|
936
929
|
|
|
937
930
|
Returns
|
|
938
931
|
-------
|
|
939
|
-
island : list of island-elms
|
|
932
|
+
island : list of island-elms.
|
|
940
933
|
"""
|
|
941
934
|
if adjacency is not None and connectivity is not None:
|
|
942
935
|
raise ValueError(f"Provide either neighbors or connectivity, not both.")
|
|
@@ -978,9 +971,12 @@ def find_island_elms(connectivity=None, adjacency=None, verbose=False, island_cr
|
|
|
978
971
|
|
|
979
972
|
elif decision == 'cumulative':
|
|
980
973
|
return np.argwhere(counter_not_visited > 0)
|
|
974
|
+
else:
|
|
975
|
+
raise ValueError
|
|
981
976
|
|
|
982
977
|
|
|
983
|
-
def cortical_depth(mesh_fn, geo_fn=None,
|
|
978
|
+
def cortical_depth(mesh_fn, geo_fn=None, skin_surface_id=1005, mesh_fn_out=None,
|
|
979
|
+
tissue_type=None, only_tri=False, verbose=False):
|
|
984
980
|
"""
|
|
985
981
|
Compute skin-cortex-distance (SCD) for surface and volume data in ``mesh_fn``.
|
|
986
982
|
|
|
@@ -997,15 +993,23 @@ def cortical_depth(mesh_fn, geo_fn=None, write_xdmf=True, skin_surface_id=1005,
|
|
|
997
993
|
geo_fn : str, optional
|
|
998
994
|
:py:class:`~pynibs.mesh.mesh_struct.TetrahedraLinear` mesh file with geometric data. If provided, geometric
|
|
999
995
|
information is read from here.
|
|
1000
|
-
write_xdmf : bool, default: True
|
|
1001
|
-
Write .xdmf or not.
|
|
1002
996
|
skin_surface_id : int, default: 1005
|
|
1003
997
|
Which tissue type nr to compute distance against.
|
|
998
|
+
mesh_fn_out : str, optional
|
|
999
|
+
If provided, SCD information is written to this file.
|
|
1000
|
+
tissue_type : list of int, optional
|
|
1001
|
+
For which tissue types to compute SCD. If None, all tissue types are computed.
|
|
1002
|
+
only_tri : bool, default: False
|
|
1003
|
+
If True, only triangles are computed.
|
|
1004
1004
|
verbose : bool, default: False
|
|
1005
1005
|
Print some verbosity information.
|
|
1006
1006
|
|
|
1007
1007
|
Returns
|
|
1008
1008
|
-------
|
|
1009
|
+
distances_tri : np.ndarray
|
|
1010
|
+
Distances of triangles to skin surface.
|
|
1011
|
+
distances_tets : np.ndarray
|
|
1012
|
+
Distances of tetrahedra to skin surface.
|
|
1009
1013
|
<file> : .hdf5
|
|
1010
1014
|
``mesh_fn`` or ``geo_fn`` with SCD information in ``/data/tris/Cortex_dist`` and ``/data/tets/Cortex_dist``.
|
|
1011
1015
|
<file> : .xdmf
|
|
@@ -1017,11 +1021,11 @@ def cortical_depth(mesh_fn, geo_fn=None, write_xdmf=True, skin_surface_id=1005,
|
|
|
1017
1021
|
skin_tri_idx = f['/mesh/elm/tri_tissue_type'][:] == skin_surface_id
|
|
1018
1022
|
tri_nodes = f['/mesh/elm/triangle_number_list'][:][skin_tri_idx]
|
|
1019
1023
|
|
|
1020
|
-
elms_with_island, counter_visited, counter_not_visited =
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
hdf5 = pynibs.load_mesh_hdf5(mesh_fn)
|
|
1024
|
+
elms_with_island, counter_visited, counter_not_visited = find_islands(connectivity=tri_nodes,
|
|
1025
|
+
verbose=True,
|
|
1026
|
+
island_crit='edge',
|
|
1027
|
+
largest=True)
|
|
1028
|
+
hdf5 = pynibs.hdf5_io.load_mesh_hdf5(mesh_fn)
|
|
1025
1029
|
|
|
1026
1030
|
with h5py.File(geo_fn, 'r') as geo:
|
|
1027
1031
|
# get indices for skin elements
|
|
@@ -1031,25 +1035,37 @@ def cortical_depth(mesh_fn, geo_fn=None, write_xdmf=True, skin_surface_id=1005,
|
|
|
1031
1035
|
def fun(row):
|
|
1032
1036
|
return np.min(np.linalg.norm(row - skin_positions, axis=1))
|
|
1033
1037
|
|
|
1038
|
+
if tissue_type is not None:
|
|
1039
|
+
arr_tri = hdf5.triangles_center[np.isin(hdf5.triangles_regions, tissue_type)]
|
|
1040
|
+
arr_tet = hdf5.tetrahedra_center[np.isin(hdf5.tetrahedra_regions, tissue_type)]
|
|
1041
|
+
else:
|
|
1042
|
+
arr_tri = hdf5.triangles_center
|
|
1043
|
+
arr_tet = hdf5.tetrahedra_center
|
|
1044
|
+
|
|
1034
1045
|
if verbose:
|
|
1035
1046
|
print("Computing triangles")
|
|
1036
|
-
distances_tri = np.apply_along_axis(fun, axis=1, arr=
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1047
|
+
distances_tri = np.apply_along_axis(fun, axis=1, arr=arr_tri)
|
|
1048
|
+
|
|
1049
|
+
if not only_tri:
|
|
1050
|
+
if verbose:
|
|
1051
|
+
print("Computing tetrahedra")
|
|
1052
|
+
distances_tets = np.apply_along_axis(fun, axis=1, arr=arr_tet)
|
|
1053
|
+
else:
|
|
1054
|
+
distances_tets = None
|
|
1055
|
+
if mesh_fn_out is not None:
|
|
1056
|
+
with h5py.File(mesh_fn_out, 'a') as f:
|
|
1057
|
+
try:
|
|
1058
|
+
del f['data/tris/Cortex_dist']
|
|
1059
|
+
del f['data/tets/Cortex_dist']
|
|
1060
|
+
except KeyError:
|
|
1061
|
+
pass
|
|
1062
|
+
f.create_dataset(name='data/tris/Cortex_dist', data=distances_tri)
|
|
1063
|
+
if not only_tri:
|
|
1064
|
+
f.create_dataset(name='data/tets/Cortex_dist', data=distances_tets)
|
|
1065
|
+
|
|
1051
1066
|
pynibs.write_xdmf(overwrite_xdmf=True, hdf5_geo_fn=geo_fn, hdf5_fn=mesh_fn)
|
|
1052
1067
|
|
|
1068
|
+
return distances_tri, distances_tets
|
|
1053
1069
|
|
|
1054
1070
|
def calc_distances(coords, mesh_fn, tissues=None):
|
|
1055
1071
|
"""
|
|
@@ -1067,8 +1083,7 @@ def calc_distances(coords, mesh_fn, tissues=None):
|
|
|
1067
1083
|
Returns
|
|
1068
1084
|
-------
|
|
1069
1085
|
distances : pd.Dataframe()
|
|
1070
|
-
colunms: coorrd, tissue_type, distance
|
|
1071
|
-
|
|
1086
|
+
colunms: coorrd, tissue_type, distance.
|
|
1072
1087
|
"""
|
|
1073
1088
|
coords = np.atleast_2d(coords)
|
|
1074
1089
|
print("Coordinate | tissue type | Distance")
|
|
@@ -1101,3 +1116,80 @@ def calc_distances(coords, mesh_fn, tissues=None):
|
|
|
1101
1116
|
print(f"{coord} | {tissue: >4} | {distances.round(2): >6} mm")
|
|
1102
1117
|
print("-" * 40)
|
|
1103
1118
|
return pd.DataFrame().from_dict(res)
|
|
1119
|
+
|
|
1120
|
+
|
|
1121
|
+
def head_circumference(fn_hdf5, z_center=None, result_folder=None):
|
|
1122
|
+
"""
|
|
1123
|
+
Computes the head circumference of a mesh.
|
|
1124
|
+
|
|
1125
|
+
.. figure:: ../../doc/images/head_circum.png
|
|
1126
|
+
:scale: 50 %
|
|
1127
|
+
:alt: Head circumference
|
|
1128
|
+
|
|
1129
|
+
For each point on the circumference, the closest point on the skin surface is determined.
|
|
1130
|
+
Distances from point to point are summed up to get the head circumference.
|
|
1131
|
+
|
|
1132
|
+
Parameters
|
|
1133
|
+
----------
|
|
1134
|
+
fn_hdf5 : str
|
|
1135
|
+
Filename of the mesh in hdf5 format.
|
|
1136
|
+
z_center : float, optional
|
|
1137
|
+
Z-coordinate of the center of the head. If not provided, it is computed from the mesh.
|
|
1138
|
+
result_folder : str, optional
|
|
1139
|
+
Folder to save the results. If not provided, the results are not saved.
|
|
1140
|
+
|
|
1141
|
+
Returns
|
|
1142
|
+
-------
|
|
1143
|
+
circ : float
|
|
1144
|
+
Head circumference in mm.
|
|
1145
|
+
"""
|
|
1146
|
+
with h5py.File(fn_hdf5, 'r') as f:
|
|
1147
|
+
nodes = f['mesh/nodes/node_coord'][:]
|
|
1148
|
+
# tris = f['mesh/elm/node_number_list'][:]
|
|
1149
|
+
tri_tissue_type = f['/mesh/elm/tri_tissue_type'][:]
|
|
1150
|
+
tr_con = f['/mesh/elm/triangle_number_list'][:]
|
|
1151
|
+
|
|
1152
|
+
if z_center is None:
|
|
1153
|
+
# find the highest point of eyes
|
|
1154
|
+
eye_surface_id = 1006
|
|
1155
|
+
eye_positions = nodes[tr_con[tri_tissue_type == 1006]]
|
|
1156
|
+
eye_top = eye_positions[:, :, 2].max()
|
|
1157
|
+
dist = 40
|
|
1158
|
+
z_center = eye_top + dist
|
|
1159
|
+
|
|
1160
|
+
n = 100
|
|
1161
|
+
r = 200
|
|
1162
|
+
origins = np.array(
|
|
1163
|
+
[(math.cos(2 * np.pi / n * x) * r, math.sin(2 * np.pi / n * x) * r, z_center) for x in range(0, n + 1)])
|
|
1164
|
+
directions = -origins.copy()
|
|
1165
|
+
directions[:, 2] = 0
|
|
1166
|
+
skin_mesh = trimesh.Trimesh(vertices=nodes, faces=tr_con[tri_tissue_type == 1005])
|
|
1167
|
+
hits, idx_ray, idx_tri = skin_mesh.ray.intersects_location(
|
|
1168
|
+
ray_origins=origins, ray_directions=directions, multiple_hits=True)
|
|
1169
|
+
|
|
1170
|
+
closest_hits = []
|
|
1171
|
+
for idx in range(origins.shape[0]):
|
|
1172
|
+
ray_idx = np.squeeze(np.argwhere(idx_ray == idx))
|
|
1173
|
+
min_idx = np.argmin(np.linalg.norm(origins[idx] - hits[ray_idx], axis=1))
|
|
1174
|
+
closest_hits.append(hits[ray_idx][min_idx])
|
|
1175
|
+
closest_hits = np.array(closest_hits)
|
|
1176
|
+
|
|
1177
|
+
if result_folder is not None:
|
|
1178
|
+
os.makedirs(result_folder, exist_ok=True)
|
|
1179
|
+
|
|
1180
|
+
meshio.Mesh(
|
|
1181
|
+
points=closest_hits,
|
|
1182
|
+
cells=[('vertex', np.array([[i, ] for i in range(closest_hits.shape[0])])), ],
|
|
1183
|
+
point_data={'hits': list(range(closest_hits.shape[0]))}).write(
|
|
1184
|
+
f"{result_folder}/hits.vtk")
|
|
1185
|
+
|
|
1186
|
+
meshio.Mesh(
|
|
1187
|
+
points=origins,
|
|
1188
|
+
cells=[('vertex', np.array([[i, ] for i in range(len(hits[0]))])), ],
|
|
1189
|
+
point_data={'hits': list(range(len(hits[0])))}).write(
|
|
1190
|
+
f"{result_folder}/origins.vtk")
|
|
1191
|
+
|
|
1192
|
+
dist = np.linalg.norm(closest_hits[-1] - closest_hits[0])
|
|
1193
|
+
for idx in range(closest_hits.shape[0] - 1):
|
|
1194
|
+
dist += np.linalg.norm(closest_hits[idx] - closest_hits[idx + 1])
|
|
1195
|
+
return np.round(dist, 2)
|
pynibs/models/_TMS.py
CHANGED
|
@@ -6,6 +6,7 @@ import pynibs
|
|
|
6
6
|
import simnibs
|
|
7
7
|
import datetime
|
|
8
8
|
import numpy as np
|
|
9
|
+
|
|
9
10
|
try:
|
|
10
11
|
from pygpc.AbstractModel import AbstractModel
|
|
11
12
|
except ImportError:
|
|
@@ -179,7 +180,7 @@ class _TMS(AbstractModel):
|
|
|
179
180
|
# dipole position and magnitude
|
|
180
181
|
fn_coil_geo = glob.glob(os.path.join(S.pathfem, "*.geo"))[0]
|
|
181
182
|
print("Reading coil dipole information from {}".format(fn_coil_geo))
|
|
182
|
-
dipole_position, dipole_moment_mag = pynibs.
|
|
183
|
+
dipole_position, dipole_moment_mag = pynibs.util.simnibs_io.read_coil_geo(fn_coil_geo)
|
|
183
184
|
|
|
184
185
|
# Additional information
|
|
185
186
|
#######################################################################
|
pynibs/muap.py
CHANGED
|
@@ -84,7 +84,6 @@ def create_electrode(l_x, l_z, n_x, n_z):
|
|
|
84
84
|
electrode_coords : ndarray of float [n_ele x 3]
|
|
85
85
|
Coordinates of point electrodes (x, y, z)
|
|
86
86
|
"""
|
|
87
|
-
|
|
88
87
|
electrode_coords = np.zeros((n_x*n_z, 3))
|
|
89
88
|
|
|
90
89
|
i = 0
|
|
@@ -389,4 +388,4 @@ def calc_mep_wilson(firing_rate_in, t, Qvmax=900, Qmmax=300, q=8, Tmin=14, N=100
|
|
|
389
388
|
for tau in spike_times[k]:
|
|
390
389
|
mep += Mk[k] * hermite_rodriguez_1st(t=t, tau0=tau0, tau=tau, lam=lam)
|
|
391
390
|
|
|
392
|
-
return mep
|
|
391
|
+
return mep
|
pynibs/neuron/__init__.py
CHANGED
|
@@ -1,2 +1,12 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Functions to compute directional-sensitivity-informed [1]_ neuronal activation thresholds [2]_.
|
|
3
|
+
|
|
4
|
+
.. [1] Jing, Y., Numssen, O., Hartwigsen, G., Knösche, T. R., & Weise, K. (2024). Effects of Electric Field Direction
|
|
5
|
+
on TMS-based Motor Cortex Mapping. *bioRxiv*
|
|
6
|
+
`10.1101/2024.12.10.627753 <https://doi.org/10.1101/2024.12.10.627753>`_
|
|
7
|
+
.. [2] Weise, K., Makaroff, S. N., Numssen, O., Bikson, M., & Knösche, T. R. (2025). Statistical method accounts for
|
|
8
|
+
microscopic electric field distortions around neurons when simulating activation thresholds. *Brain Stimulation,
|
|
9
|
+
18*(2), 280–286. `10.1016/j.brs.2025.02.007 <https://doi.org/10.1016/j.brs.2025.02.007>`_
|
|
10
|
+
"""
|
|
1
11
|
from .neuron_regression import *
|
|
2
12
|
from .util import *
|