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
|
@@ -1,13 +1,8 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Functions that operate on region of interest (ROI) data.
|
|
3
|
-
"""
|
|
4
1
|
import os
|
|
5
2
|
import h5py
|
|
6
|
-
import math
|
|
7
3
|
import tqdm
|
|
8
4
|
import time
|
|
9
5
|
import scipy
|
|
10
|
-
import trimesh
|
|
11
6
|
import nibabel
|
|
12
7
|
import warnings
|
|
13
8
|
import platform
|
|
@@ -135,6 +130,33 @@ def load_roi_surface_obj_from_hdf5(fname):
|
|
|
135
130
|
return read_roi_from_mesh_hdf5(fname)
|
|
136
131
|
|
|
137
132
|
|
|
133
|
+
def read_roi_from_geo_fn(geo_fn):
|
|
134
|
+
"""
|
|
135
|
+
Creates pynibs.RegionOfInterest from geo.hdf5
|
|
136
|
+
|
|
137
|
+
Parameters
|
|
138
|
+
----------
|
|
139
|
+
geo_fn : str
|
|
140
|
+
|
|
141
|
+
Returns
|
|
142
|
+
-------
|
|
143
|
+
roi : pynibs.RegionOfInterest
|
|
144
|
+
"""
|
|
145
|
+
roi = pynibs.RegionOfInterestSurface()
|
|
146
|
+
with h5py.File(geo_fn, 'r') as f:
|
|
147
|
+
roi.n_tris = f['mesh/elm/triangle_number_list'][:].shape[0]
|
|
148
|
+
roi.n_nodes = f['mesh/nodes/node_coord'][:].shape[0]
|
|
149
|
+
roi.n_tets = -1
|
|
150
|
+
roi.node_coord_mid = f['mesh/nodes/node_coord'][:]
|
|
151
|
+
roi.node_number_list = f['mesh/elm/triangle_number_list'][:]
|
|
152
|
+
if roi.node_number_list.min() == 1:
|
|
153
|
+
roi.node_number_list = - 1
|
|
154
|
+
tri_coords = roi.node_coord_mid[roi.node_number_list]
|
|
155
|
+
roi.tri_center_coord_mid = np.mean(tri_coords, axis=1)
|
|
156
|
+
|
|
157
|
+
return roi
|
|
158
|
+
|
|
159
|
+
|
|
138
160
|
def read_roi_from_mesh_hdf5(fname, roi_id=None):
|
|
139
161
|
"""
|
|
140
162
|
Loading and initializing RegionOfInterestSurface object/s from .hdf5 mesh file.
|
|
@@ -387,6 +409,7 @@ def make_GM_WM_surface(gm_surf_fname, wm_surf_fname, mesh_folder, midlayer_surf_
|
|
|
387
409
|
|
|
388
410
|
* 0 -> WM surface
|
|
389
411
|
* 1 -> GM surface
|
|
412
|
+
|
|
390
413
|
x_roi : list of float or None
|
|
391
414
|
Region of interest [Xmin, Xmax], whole X range if empty [0,0] or None
|
|
392
415
|
(left - right)
|
|
@@ -401,8 +424,9 @@ def make_GM_WM_surface(gm_surf_fname, wm_surf_fname, mesh_folder, midlayer_surf_
|
|
|
401
424
|
|
|
402
425
|
* 1: one layer
|
|
403
426
|
* 3: additionally upper and lower layers are generated around the central midlayer
|
|
427
|
+
|
|
404
428
|
fn_mask: string or None
|
|
405
|
-
Filename for freesurfer mask. If given, this is used instead of
|
|
429
|
+
Filename for freesurfer mask. If given, this is used instead of \\*_ROIs
|
|
406
430
|
refine : bool, optional, default: False
|
|
407
431
|
Refine ROI by splitting elements
|
|
408
432
|
|
|
@@ -424,15 +448,14 @@ def make_GM_WM_surface(gm_surf_fname, wm_surf_fname, mesh_folder, midlayer_surf_
|
|
|
424
448
|
connectivity : np.ndarray of int
|
|
425
449
|
(N_tri x 3) Connectivity of triangles (indexation starts at 0!)
|
|
426
450
|
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
451
|
+
Examples
|
|
452
|
+
--------
|
|
430
453
|
.. code-block:: python
|
|
431
454
|
|
|
432
455
|
make_GM_WM_surface(self, gm_surf_fname, wm_surf_fname, delta, X_ROI, Y_ROI, Z_ROI)
|
|
433
456
|
make_GM_WM_surface(self, gm_surf_fname, wm_surf_fname, delta, mask_fn, layer=3)
|
|
434
457
|
"""
|
|
435
|
-
|
|
458
|
+
import trimesh
|
|
436
459
|
if type(gm_surf_fname) is not list:
|
|
437
460
|
gm_surf_fname = [gm_surf_fname]
|
|
438
461
|
|
|
@@ -460,7 +483,7 @@ def make_GM_WM_surface(gm_surf_fname, wm_surf_fname, mesh_folder, midlayer_surf_
|
|
|
460
483
|
for i in range(len(gm_surf_fname)):
|
|
461
484
|
if gm_surf_fname[i] is not None:
|
|
462
485
|
if gm_surf_fname[i].endswith('.gii'):
|
|
463
|
-
img = nibabel.
|
|
486
|
+
img = nibabel.load(os.path.join(mesh_folder, gm_surf_fname[i]))
|
|
464
487
|
points_gm[i] = img.agg_data('pointset')
|
|
465
488
|
con_gm[i] = img.agg_data('triangle')
|
|
466
489
|
else:
|
|
@@ -470,7 +493,7 @@ def make_GM_WM_surface(gm_surf_fname, wm_surf_fname, mesh_folder, midlayer_surf_
|
|
|
470
493
|
|
|
471
494
|
if wm_surf_fname[i] is not None:
|
|
472
495
|
if wm_surf_fname[i].endswith('.gii'):
|
|
473
|
-
img = nibabel.
|
|
496
|
+
img = nibabel.load(os.path.join(mesh_folder, wm_surf_fname[i]))
|
|
474
497
|
points_wm[i] = img.agg_data('pointset')
|
|
475
498
|
con_wm[i] = img.agg_data('triangle')
|
|
476
499
|
else:
|
|
@@ -480,12 +503,12 @@ def make_GM_WM_surface(gm_surf_fname, wm_surf_fname, mesh_folder, midlayer_surf_
|
|
|
480
503
|
|
|
481
504
|
if midlayer_surf_fname[i] is not None:
|
|
482
505
|
if midlayer_surf_fname[i].endswith('.gii'):
|
|
483
|
-
img = nibabel.
|
|
506
|
+
img = nibabel.load(os.path.join(mesh_folder, midlayer_surf_fname[i]))
|
|
484
507
|
points_mid[i] = img.agg_data('pointset')
|
|
485
508
|
con_mid[i] = img.agg_data('triangle')
|
|
486
509
|
else:
|
|
487
510
|
points_mid[i], con_mid[i] = nib.freesurfer.read_geometry(
|
|
488
|
-
|
|
511
|
+
os.path.join(mesh_folder, midlayer_surf_fname[i]))
|
|
489
512
|
con_mid[i] = con_mid[i] + max_idx_mid
|
|
490
513
|
max_idx_mid = max_idx_mid + points_mid[i].shape[0] # np.max(con_wm[i]) + 2
|
|
491
514
|
|
|
@@ -588,13 +611,13 @@ def make_GM_WM_surface(gm_surf_fname, wm_surf_fname, mesh_folder, midlayer_surf_
|
|
|
588
611
|
mesh.export(roi_fn)
|
|
589
612
|
|
|
590
613
|
roi_refined_fn = os.path.join(mesh_folder, "", "tmp", "roi_refined.stl")
|
|
591
|
-
pynibs.refine_surface(fn_surf=roi_fn,
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
614
|
+
pynibs.mesh.refine_surface(fn_surf=roi_fn,
|
|
615
|
+
fn_surf_refined=roi_refined_fn,
|
|
616
|
+
center=[0, 0, 0],
|
|
617
|
+
radius=np.inf,
|
|
618
|
+
verbose=True,
|
|
619
|
+
repair=False,
|
|
620
|
+
remesh=False)
|
|
598
621
|
|
|
599
622
|
roi = trimesh.load(roi_refined_fn)
|
|
600
623
|
con_cropped_reform = roi.faces
|
|
@@ -608,12 +631,12 @@ def make_GM_WM_surface(gm_surf_fname, wm_surf_fname, mesh_folder, midlayer_surf_
|
|
|
608
631
|
faces=con_cropped_reform)
|
|
609
632
|
mesh.export(roi_fn)
|
|
610
633
|
|
|
611
|
-
pynibs.refine_surface(fn_surf=roi_fn,
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
634
|
+
pynibs.mesh.refine_surface(fn_surf=roi_fn,
|
|
635
|
+
fn_surf_refined=roi_refined_fn,
|
|
636
|
+
center=[0, 0, 0],
|
|
637
|
+
radius=np.inf,
|
|
638
|
+
verbose=True,
|
|
639
|
+
repair=False)
|
|
617
640
|
|
|
618
641
|
roi.append(trimesh.load(roi_refined_fn))
|
|
619
642
|
|
|
@@ -776,12 +799,12 @@ def create_refine_spherical_roi(center, radius, final_tissues_nii, out_fn, targe
|
|
|
776
799
|
"""
|
|
777
800
|
# get spherical ROI, masked to all tissues
|
|
778
801
|
roi_img = get_sphere_in_nii(
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
802
|
+
center=center,
|
|
803
|
+
radius=radius,
|
|
804
|
+
nii=final_tissues_nii,
|
|
805
|
+
val_in=target_size,
|
|
806
|
+
out_fn=out_spher_fn,
|
|
807
|
+
outside_val=outside_size, outside_radius=radius * outside_factor)
|
|
785
808
|
|
|
786
809
|
# get tissue_types data
|
|
787
810
|
if isinstance(final_tissues_nii, str):
|
pynibs/subject.py
CHANGED
|
@@ -28,7 +28,7 @@ class Subject:
|
|
|
28
28
|
id : str
|
|
29
29
|
Subject id.
|
|
30
30
|
fn_mesh : str
|
|
31
|
-
|
|
31
|
+
.msh or .hdf5 file containing the mesh information.
|
|
32
32
|
|
|
33
33
|
**Subject.seg, segmentation information dictionary**
|
|
34
34
|
|
|
@@ -90,7 +90,7 @@ class Subject:
|
|
|
90
90
|
**Subject.mesh, mesh dictionary**
|
|
91
91
|
|
|
92
92
|
info : str
|
|
93
|
-
Information about the mesh (e.g.
|
|
93
|
+
Information about the mesh (e.g. discretization, etc.).
|
|
94
94
|
fn_mesh_msh : str
|
|
95
95
|
Filename of the .msh file containing the FEM mesh.
|
|
96
96
|
fn_mesh_hdf5 : str
|
|
@@ -522,7 +522,7 @@ def save_subject_hdf5(subject_id, subject_folder, fname, mri_dict=None, mesh_dic
|
|
|
522
522
|
mesh_dict = {i: mesh_dict[i] for i in range(len(mesh_dict))}
|
|
523
523
|
|
|
524
524
|
for mesh_name, mesh_dict in mesh_dict.items():
|
|
525
|
-
mesh = pynibs.Mesh(mesh_name=mesh_name, subject_id=subject_id, subject_folder=subject_folder)
|
|
525
|
+
mesh = pynibs.mesh.Mesh(mesh_name=mesh_name, subject_id=subject_id, subject_folder=subject_folder)
|
|
526
526
|
mesh.fill_defaults(mesh_dict['approach'])
|
|
527
527
|
mesh = fill_from_dict(mesh, mesh_dict)
|
|
528
528
|
mesh.write_to_hdf5(fn_hdf5=fname, check_file_exist=check_file_exist, verbose=verbose)
|
|
@@ -530,7 +530,7 @@ def save_subject_hdf5(subject_id, subject_folder, fname, mri_dict=None, mesh_dic
|
|
|
530
530
|
if roi_dict is not None:
|
|
531
531
|
for mesh_name in roi_dict.keys():
|
|
532
532
|
for roi_name, roi_dict_i in roi_dict[mesh_name].items():
|
|
533
|
-
roi = pynibs.ROI(subject_id=subject_id, roi_name=roi_name, mesh_name=mesh_name)
|
|
533
|
+
roi = pynibs.mesh.ROI(subject_id=subject_id, roi_name=roi_name, mesh_name=mesh_name)
|
|
534
534
|
roi = fill_from_dict(roi, roi_dict_i)
|
|
535
535
|
roi.write_to_hdf5(fn_hdf5=fname, check_file_exist=check_file_exist, verbose=verbose)
|
|
536
536
|
|
|
@@ -717,7 +717,7 @@ def load_subject_pkl(fname):
|
|
|
717
717
|
fn_scipt = os.path.join(os.path.split(fname)[0],
|
|
718
718
|
"create_subject_" +
|
|
719
719
|
os.path.splitext(os.path.split(fname)[1])[0] + ".py")
|
|
720
|
-
pynibs.bash_call("python {}".format(fn_scipt))
|
|
720
|
+
pynibs.util.utils.bash_call("python {}".format(fn_scipt))
|
|
721
721
|
|
|
722
722
|
with open(fname, 'rb') as f:
|
|
723
723
|
return pickle.load(f)
|
|
@@ -761,20 +761,21 @@ def create_plot_settings_dict(plotfunction_type):
|
|
|
761
761
|
* 'volume_plot'
|
|
762
762
|
* 'volume_plot_vtu'
|
|
763
763
|
|
|
764
|
+
|
|
764
765
|
Returns
|
|
765
766
|
-------
|
|
766
767
|
ps : dict
|
|
767
768
|
Dictionary containing the plotsettings.
|
|
768
769
|
axes : bool
|
|
769
770
|
Show orientation axes.
|
|
770
|
-
background_color :
|
|
771
|
+
background_color : np.ndarray
|
|
771
772
|
(1m 3) Set background color of exported image RGB (0...1).
|
|
772
773
|
calculator : str
|
|
773
774
|
Format string with placeholder of the calculator expression the quantity to plot is modified with,
|
|
774
775
|
e.g.: "{}^5".
|
|
775
|
-
clip_coords :
|
|
776
|
+
clip_coords : np.ndarray of float
|
|
776
777
|
(N_clips, 3) Coordinates of clip surface origins (x,y,z).
|
|
777
|
-
clip_normals :
|
|
778
|
+
clip_normals : np.ndarray of float
|
|
778
779
|
(N_clips, 3) Surface normals of clip surfaces pointing in the direction where the volume is kept for
|
|
779
780
|
clip_type = ['clip' ...] (x,y,z).
|
|
780
781
|
clip_type : list of str
|
|
@@ -782,16 +783,18 @@ def create_plot_settings_dict(plotfunction_type):
|
|
|
782
783
|
|
|
783
784
|
* 'clip': cut geometry but keep volume behind
|
|
784
785
|
* 'slice': cut geometry and keep only the slice
|
|
786
|
+
|
|
785
787
|
coil_dipole_scaling : list [1 x 2]
|
|
786
788
|
Specify the scaling type of the dipoles (2 entries):
|
|
787
789
|
``coil_dipole_scaling[0]``:
|
|
788
790
|
|
|
789
791
|
* 'uniform': uniform scaling, i.e. all dipoles have the same size
|
|
790
|
-
|
|
792
|
+
* 'scaled': size scaled according to dipole magnitude
|
|
791
793
|
|
|
792
794
|
``coil_dipole_scaling[1]``:
|
|
793
795
|
|
|
794
796
|
* scalar scale parameter of dipole size
|
|
797
|
+
|
|
795
798
|
coil_dipole_color : str or list
|
|
796
799
|
Color of the dipoles; either str to specify colormap (e.g. 'jet') or list of RGB values [1 x 3] (0...1).
|
|
797
800
|
coil_axes : bool, default: True
|
|
@@ -814,8 +817,8 @@ def create_plot_settings_dict(plotfunction_type):
|
|
|
814
817
|
maximum number of colorbar labels.
|
|
815
818
|
colorbar_labelcolor : list of float
|
|
816
819
|
(1, 3) Color of colorbar labels in RGB (0...1).
|
|
817
|
-
colormap : str or
|
|
818
|
-
If
|
|
820
|
+
colormap : str or np.ndarray
|
|
821
|
+
If np.ndarray [1 x 4*N]: custom colormap providing data and corresponding RGB values
|
|
819
822
|
|
|
820
823
|
.. math::
|
|
821
824
|
\\begin{bmatrix}
|
|
@@ -839,9 +842,10 @@ def create_plot_settings_dict(plotfunction_type):
|
|
|
839
842
|
* 'Viridis (matplotlib)',
|
|
840
843
|
* 'gray_Matlab',
|
|
841
844
|
* 'Spectral_lowBlue',
|
|
842
|
-
|
|
845
|
+
* 'BuRd'
|
|
843
846
|
* 'Rainbow Blended White'
|
|
844
847
|
* 'b2rcw'
|
|
848
|
+
|
|
845
849
|
colormap_categories : bool
|
|
846
850
|
Use categorized (discrete) colormap.
|
|
847
851
|
datarange : list
|
|
@@ -857,6 +861,7 @@ def create_plot_settings_dict(plotfunction_type):
|
|
|
857
861
|
* 3 -> cerebrospinal fluid (CSF)
|
|
858
862
|
* 4 -> skull
|
|
859
863
|
* 5 -> skin
|
|
864
|
+
|
|
860
865
|
domain_label : str
|
|
861
866
|
Label of the dataset which contains the domain IDs (default: 'tissue_type').
|
|
862
867
|
edges : BOOL
|
|
@@ -868,6 +873,7 @@ def create_plot_settings_dict(plotfunction_type):
|
|
|
868
873
|
* .hdf5-file(s): filename(s) of .hdf5 file(s) containing the data and the geometry. The data can be provided
|
|
869
874
|
in the first hdf5 file and the geometry can be provided in the second file. However, both can be also
|
|
870
875
|
provided in a single hdf5 file.
|
|
876
|
+
|
|
871
877
|
fname_png : str
|
|
872
878
|
Name of output .png file (incl. path).
|
|
873
879
|
fname_vtu_volume : str
|
|
@@ -893,6 +899,7 @@ def create_plot_settings_dict(plotfunction_type):
|
|
|
893
899
|
... & ... & ... & ...\\\\
|
|
894
900
|
data_{N} & opac_N & 0.5 & 0 \\\\
|
|
895
901
|
\\end{bmatrix}
|
|
902
|
+
|
|
896
903
|
plot_function : str
|
|
897
904
|
Function the plot is generated with:
|
|
898
905
|
|
|
@@ -900,6 +907,7 @@ def create_plot_settings_dict(plotfunction_type):
|
|
|
900
907
|
- 'surface_vector_plot_vtu'
|
|
901
908
|
- 'volume_plot'
|
|
902
909
|
- 'volume_plot_vtu'
|
|
910
|
+
|
|
903
911
|
png_resolution : float
|
|
904
912
|
Resolution parameter of output image (1...5).
|
|
905
913
|
quantity : str
|
|
@@ -924,10 +932,11 @@ def create_plot_settings_dict(plotfunction_type):
|
|
|
924
932
|
- 'All Points' (not set)
|
|
925
933
|
- 'Every Nth Point' (every Nth vector is shown in the grid)
|
|
926
934
|
- 'Uniform Spatial Distribution' (not set)
|
|
935
|
+
|
|
927
936
|
view : list
|
|
928
937
|
Camera position and angle in 3D space:
|
|
929
938
|
``[[3 x CameraPosition], [3 x CameraFocalPoint], [3 x CameraViewUp], 1 x CameraParallelScale]``.
|
|
930
|
-
viewsize :
|
|
939
|
+
viewsize : np.ndarray [1 x 2]
|
|
931
940
|
Set size of exported image in pixel [width x height] will be extra scaled by parameter png_resolution.
|
|
932
941
|
vlabels : list of str
|
|
933
942
|
Labels of vector datasets to plot (other present datasets are ignored).
|
|
@@ -937,7 +946,8 @@ def create_plot_settings_dict(plotfunction_type):
|
|
|
937
946
|
List containing the type of vector scaling:
|
|
938
947
|
|
|
939
948
|
- 'off': all vectors are normalized
|
|
940
|
-
- 'vector': vectors are scaled according to their
|
|
949
|
+
- 'vector': vectors are scaled according to their magnitudes
|
|
950
|
+
|
|
941
951
|
"""
|
|
942
952
|
if plotfunction_type not in ['surface_vector_plot', 'surface_vector_plot_vtu', 'volume_plot', 'volume_plot_vtu']:
|
|
943
953
|
raise Exception('plotfunction_type not set correctly, specify either [\'surface_vector_plot\', \
|
pynibs/util/__init__.py
CHANGED
|
@@ -1,4 +1,20 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
import importlib
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
_lazy_modules = [
|
|
5
|
+
"simnibs_io",
|
|
6
|
+
"utils",
|
|
7
|
+
"quality_measures",
|
|
8
|
+
"rotations"
|
|
9
|
+
]
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def __getattr__(name):
|
|
13
|
+
"""
|
|
14
|
+
Lazy-load selected submodules when accessed as attributes of pynibs.util.
|
|
15
|
+
"""
|
|
16
|
+
if name in _lazy_modules:
|
|
17
|
+
mod = importlib.import_module(f".{name}", __name__)
|
|
18
|
+
globals()[name] = mod # cache for future accesses
|
|
19
|
+
return mod
|
|
20
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
pynibs/util/dosing.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
"""
|
|
2
|
-
Functions used in our perspective on e-field based TMS dosing[1]_
|
|
2
|
+
Functions used in our perspective on e-field based TMS dosing [1]_
|
|
3
3
|
|
|
4
4
|
References
|
|
5
5
|
----------
|
|
@@ -46,7 +46,6 @@ def get_intensity_e(e1, e2, target1, target2, radius1, radius2, headmesh,
|
|
|
46
46
|
rmt_e_corr : float
|
|
47
47
|
Adjusted stimulation intensity for target2.
|
|
48
48
|
"""
|
|
49
|
-
|
|
50
49
|
with h5py.File(headmesh, 'r') as f:
|
|
51
50
|
tris = f[f'/roi_surface/{roi}/tri_center_coord_mid'][:]
|
|
52
51
|
|
|
@@ -111,14 +110,14 @@ def get_intensity_e_old(mesh1, mesh2, target1, target2, radius1, radius2, rmt=1,
|
|
|
111
110
|
if os.path.splitext(mesh1)[1] == ".msh":
|
|
112
111
|
mesh1 = read_msh(mesh1)
|
|
113
112
|
elif os.path.splitext(mesh1)[1] == ".hdf5":
|
|
114
|
-
mesh1 = pynibs.load_mesh_hdf5(mesh1)
|
|
113
|
+
mesh1 = pynibs.hdf5_io.load_mesh_hdf5(mesh1)
|
|
115
114
|
|
|
116
115
|
# load mesh2 (target) if filename is provided
|
|
117
116
|
if isinstance(mesh2, str):
|
|
118
117
|
if os.path.splitext(mesh2)[1] == ".msh":
|
|
119
118
|
mesh2 = read_msh(mesh2)
|
|
120
119
|
elif os.path.splitext(mesh2)[1] == ".hdf5":
|
|
121
|
-
mesh2 = pynibs.load_mesh_hdf5(mesh2)
|
|
120
|
+
mesh2 = pynibs.hdf5_io.load_mesh_hdf5(mesh2)
|
|
122
121
|
|
|
123
122
|
# load electric fields in midlayer and average electric field around sphere in targets
|
|
124
123
|
e_avg_target = []
|
|
@@ -196,7 +195,7 @@ def get_intensity_stokes(mesh, target1, target2, spat_grad=3, rmt=0, scalp_tag=1
|
|
|
196
195
|
if os.path.splitext(mesh)[1] == ".msh":
|
|
197
196
|
mesh = read_msh(mesh)
|
|
198
197
|
elif os.path.splitext(mesh)[1] == ".hdf5":
|
|
199
|
-
mesh = pynibs.load_mesh_hdf5(mesh)
|
|
198
|
+
mesh = pynibs.hdf5_io.load_mesh_hdf5(mesh)
|
|
200
199
|
|
|
201
200
|
t1_proj = project_on_scalp(target1, mesh, scalp_tag=scalp_tag)
|
|
202
201
|
t2_proj = project_on_scalp(target2, mesh, scalp_tag=scalp_tag)
|
pynibs/util/quality_measures.py
CHANGED
|
@@ -1,8 +1,14 @@
|
|
|
1
|
-
|
|
1
|
+
"""
|
|
2
|
+
Functions used in our publication on TMS quality metrics [1]_
|
|
3
|
+
|
|
4
|
+
References
|
|
5
|
+
----------
|
|
6
|
+
.. [1] Numssen, O., Martin, S., Williams, K., Hartwigsen, G., & Knösche, T. (2024). Quantification of subject motion
|
|
7
|
+
during TMS via pulsewise coil displacement. Brain Stimulation.
|
|
8
|
+
DOI: 10.1016/j.brain.2024.07.003 #
|
|
9
|
+
"""
|
|
2
10
|
import gdist
|
|
3
|
-
|
|
4
|
-
from scipy.spatial.transform import Rotation as rot
|
|
5
|
-
|
|
11
|
+
import numpy as np
|
|
6
12
|
import pynibs
|
|
7
13
|
|
|
8
14
|
|
|
@@ -11,8 +17,8 @@ def geodesic_dist(nodes, tris, source, source_is_node=True):
|
|
|
11
17
|
Returns geodesic distance in mm from all nodes to source node (or triangle).
|
|
12
18
|
This is just a wrapper for the gdist package.
|
|
13
19
|
|
|
14
|
-
|
|
15
|
-
|
|
20
|
+
Examples
|
|
21
|
+
--------
|
|
16
22
|
|
|
17
23
|
.. code-block:: python
|
|
18
24
|
|
|
@@ -73,11 +79,11 @@ def geodesic_dist(nodes, tris, source, source_is_node=True):
|
|
|
73
79
|
|
|
74
80
|
def euclidean_dist(nodes, tris, source, source_is_node=True):
|
|
75
81
|
"""
|
|
76
|
-
Returns
|
|
82
|
+
Returns Euclidean distance of all nodes to source node (triangle).
|
|
77
83
|
This is just a wrapper for the gdist package.
|
|
78
84
|
|
|
79
|
-
|
|
80
|
-
|
|
85
|
+
Examples
|
|
86
|
+
--------
|
|
81
87
|
.. code-block:: python
|
|
82
88
|
|
|
83
89
|
with h5py.File(fn,'r') as f:
|
|
@@ -146,7 +152,6 @@ def nrmsd(array, array_ref, error_norm="relative", x_axis=False):
|
|
|
146
152
|
normalized_rms : np.ndarray of float
|
|
147
153
|
([array.shape[1]]) Normalized root-mean-square deviation between the columns of array and array_ref.
|
|
148
154
|
"""
|
|
149
|
-
|
|
150
155
|
n_points = array.shape[0]
|
|
151
156
|
|
|
152
157
|
if x_axis:
|
|
@@ -209,8 +214,6 @@ def nrmse(array, array_ref, x_axis=False):
|
|
|
209
214
|
array_ref : np.ndarray
|
|
210
215
|
reference data [ (x_ref), y0_ref, y1_ref, y2_ref ... ].
|
|
211
216
|
if array_ref is 1D, all sizes have to match.
|
|
212
|
-
error_norm : str, optional, default="relative"
|
|
213
|
-
Decide if error is determined "relative" or "absolute".
|
|
214
217
|
x_axis : bool, default: False
|
|
215
218
|
If True, the first column of array and array_ref is interpreted as the x-axis, where the data points are
|
|
216
219
|
evaluated. If False, the data points are assumed to be at the same location.
|
|
@@ -220,9 +223,6 @@ def nrmse(array, array_ref, x_axis=False):
|
|
|
220
223
|
nrmse: np.ndarray of float
|
|
221
224
|
([array.shape[1]]) Normalized root-mean-square deviation between the columns of array and array_ref.
|
|
222
225
|
"""
|
|
223
|
-
|
|
224
|
-
n_points = array.shape[0]
|
|
225
|
-
|
|
226
226
|
if x_axis:
|
|
227
227
|
# handle different array lengths
|
|
228
228
|
if len(array_ref.shape) == 1:
|
|
@@ -253,9 +253,7 @@ def nrmse(array, array_ref, x_axis=False):
|
|
|
253
253
|
data = array
|
|
254
254
|
|
|
255
255
|
# determine normalized rms deviation and return
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
return nrmse
|
|
256
|
+
return np.linalg.norm(data - data_ref, axis=0) / np.linalg.norm(data_ref, axis=0)
|
|
259
257
|
|
|
260
258
|
|
|
261
259
|
def c_map_comparison(c1, c2, t1, t2, nodes, tris):
|
|
@@ -305,11 +303,12 @@ def calc_tms_motion_params(coil_positions, reference=None):
|
|
|
305
303
|
Motion is computed w.r.t. the first ('absolute') and to the previous ('relative') stimulation.
|
|
306
304
|
|
|
307
305
|
Position shifts are quantified with respect to the subject/nifti-specific RAS coordinate system.
|
|
308
|
-
|
|
306
|
+
Rtrational changes are quantified with respect to the coil axes as follows:
|
|
307
|
+
|
|
308
|
+
* pitch: rotation around left/right axis of coil
|
|
309
|
+
* roll: rotation around coil handle axis
|
|
310
|
+
* yaw: rotation around axis from center of coil towards head
|
|
309
311
|
|
|
310
|
-
* pitch: rotation around left/right axis of coil
|
|
311
|
-
* roll: rotation around coil handle axis
|
|
312
|
-
* yaw: rotation around axis from center of coil towards head
|
|
313
312
|
Motion parameters for first coil position are set to 0.
|
|
314
313
|
|
|
315
314
|
Parameters
|
|
@@ -330,6 +329,7 @@ def calc_tms_motion_params(coil_positions, reference=None):
|
|
|
330
329
|
euler_rots_rel : np.ndarray
|
|
331
330
|
(3, n_pulses) Relative rotation angles in euler angles (alpha, beta, gamma).
|
|
332
331
|
"""
|
|
332
|
+
from scipy.spatial.transform import Rotation as rot
|
|
333
333
|
np.set_printoptions(suppress=True)
|
|
334
334
|
if reference is None:
|
|
335
335
|
i_ref = 0
|
|
@@ -346,8 +346,8 @@ def calc_tms_motion_params(coil_positions, reference=None):
|
|
|
346
346
|
reference_rot = reference[0:3, 0:3]
|
|
347
347
|
|
|
348
348
|
# compute first rotation towards reference
|
|
349
|
-
rotmat_abs = pynibs.bases2rotmat(reference_rot, coil_positions[0:3, 0:3, 0])
|
|
350
|
-
if np.isnan(rotmat_abs).any():
|
|
349
|
+
rotmat_abs = pynibs.util.rotations.bases2rotmat(reference_rot, coil_positions[0:3, 0:3, 0])
|
|
350
|
+
if np.isnan(rotmat_abs).any() or (coil_positions[0:3, 0:3, 0]**2).sum() == 3:
|
|
351
351
|
euler_rots_abs = [[np.nan, np.nan, np.nan]]
|
|
352
352
|
else:
|
|
353
353
|
euler_rots_abs = [rot.from_matrix(rotmat_abs).as_euler('xyz', degrees=True).tolist()]
|
|
@@ -363,15 +363,15 @@ def calc_tms_motion_params(coil_positions, reference=None):
|
|
|
363
363
|
n_coilpos = coil_positions.shape[2]
|
|
364
364
|
euler_rots_rel = [[0, 0, 0]]
|
|
365
365
|
for i in range(0, n_coilpos - 1):
|
|
366
|
-
rotmat_abs = pynibs.bases2rotmat(reference_rot, coil_positions[0:3, 0:3, i + 1])
|
|
367
|
-
if np.isnan(rotmat_abs).any():
|
|
366
|
+
rotmat_abs = pynibs.util.rotations.bases2rotmat(reference_rot, coil_positions[0:3, 0:3, i + 1])
|
|
367
|
+
if np.isnan(rotmat_abs).any() or (coil_positions[0:3, 0:3, i + 1]**2).sum() == 3:
|
|
368
368
|
euler_abs = [np.nan, np.nan, np.nan]
|
|
369
369
|
else:
|
|
370
370
|
euler_abs = rot.from_matrix(rotmat_abs).as_euler('xyz', degrees=True).tolist()
|
|
371
371
|
euler_rots_abs.append(euler_abs)
|
|
372
372
|
|
|
373
|
-
rotmat_rel = pynibs.bases2rotmat(coil_positions[0:3, 0:3, i], coil_positions[0:3, 0:3, i + 1])
|
|
374
|
-
if np.isnan(rotmat_rel).any():
|
|
373
|
+
rotmat_rel = pynibs.util.rotations.bases2rotmat(coil_positions[0:3, 0:3, i], coil_positions[0:3, 0:3, i + 1])
|
|
374
|
+
if np.isnan(rotmat_rel).any() or (coil_positions[0:3, 0:3, i + 1]**2).sum() == 3:
|
|
375
375
|
euler_rel = [np.nan, np.nan, np.nan]
|
|
376
376
|
else:
|
|
377
377
|
euler_rel = rot.from_matrix(rotmat_rel).as_euler('xyz', degrees=True).tolist()
|
|
@@ -410,6 +410,7 @@ def plot_tms_motion_parameter(pos_diff, euler_rots, pcd=None, fname=None):
|
|
|
410
410
|
axes : matplotlib.pyplot.axes
|
|
411
411
|
Figure axes.
|
|
412
412
|
"""
|
|
413
|
+
from matplotlib import pyplot as plt
|
|
413
414
|
if pcd is not None:
|
|
414
415
|
n_plot_rows = 3
|
|
415
416
|
else:
|
|
@@ -480,11 +481,10 @@ def compute_pcd(delta_pos, delta_rot, skin_cortex_distance=20):
|
|
|
480
481
|
|
|
481
482
|
The coil rotations (in euler angles) are transformed into a positional change projected on the cortex based on
|
|
482
483
|
``skin_cortex_distance`` as a proxy for the (local) change of the stimulation.
|
|
483
|
-
``delta_pos`` and ``delta_rot`` are expected to quantify motion w.r.t. the target coil position/
|
|
484
|
-
the absolute deltas to the first stimulation.
|
|
484
|
+
``delta_pos`` and ``delta_rot`` are expected to quantify motion w.r.t. the target coil position/rotation,
|
|
485
|
+
for example the absolute deltas to the first stimulation.
|
|
485
486
|
|
|
486
487
|
Axes definitions
|
|
487
|
-
----------------
|
|
488
488
|
``delta_pos`` and ``delta_rot`` are supposed to follow this axes definition (SimNIBS):
|
|
489
489
|
|
|
490
490
|
- delta_pos
|
|
@@ -501,21 +501,21 @@ def compute_pcd(delta_pos, delta_rot, skin_cortex_distance=20):
|
|
|
501
501
|
:alt: Coil axes definition, following SimNIBS conventions.
|
|
502
502
|
|
|
503
503
|
|
|
504
|
-
|
|
505
|
-
|
|
504
|
+
Examples
|
|
505
|
+
--------
|
|
506
506
|
.. code-block:: python
|
|
507
507
|
|
|
508
508
|
# get TriggerMarkers from a Localite session
|
|
509
509
|
mats = pynibs.read_triggermarker_localite(tm_fn)[0]
|
|
510
510
|
|
|
511
511
|
# calculate absolute and relative coil displacements
|
|
512
|
-
delta_pos_abs, delta_rot_abs, delta_pos_rel, delta_rot_rel = pynibs.calc_tms_motion_params(mats)
|
|
512
|
+
delta_pos_abs, delta_rot_abs, delta_pos_rel, delta_rot_rel = pynibs.util.quality_measures.calc_tms_motion_params(mats)
|
|
513
513
|
|
|
514
514
|
# compute PCD
|
|
515
|
-
pcd, delta_pos, delta_rot = pynibs.compute_pcd(delta_pos_abs, delta_rot_abs)
|
|
515
|
+
pcd, delta_pos, delta_rot = pynibs.util.quality_measures.compute_pcd(delta_pos_abs, delta_rot_abs)
|
|
516
516
|
|
|
517
517
|
# plot movement
|
|
518
|
-
axes = pynibs.plot_tms_motion_parameter(
|
|
518
|
+
axes = pynibs.util.quality_measures.plot_tms_motion_parameter(delta_pos_rel, delta_rot_rel, pcd)
|
|
519
519
|
matplotlib.pyplot.show()
|
|
520
520
|
|
|
521
521
|
Parameters
|
|
@@ -536,6 +536,7 @@ def compute_pcd(delta_pos, delta_rot, skin_cortex_distance=20):
|
|
|
536
536
|
delta_rot : np.ndarray
|
|
537
537
|
(n_pulses, ) sum(abs(delta_rot projected by skin_cortex_distance)) per pulse.
|
|
538
538
|
"""
|
|
539
|
+
from scipy.spatial.transform import Rotation as rot
|
|
539
540
|
# delta_pos_summed = np.sum(np.abs(delta_pos[:2, :]), axis=0)
|
|
540
541
|
delta_pos_summed = np.sqrt(np.sum(delta_pos[: 2, :] ** 2, axis=0))
|
|
541
542
|
delta_pos_summed = np.squeeze(delta_pos_summed + delta_pos[2, :] ** 2 * np.sign(delta_pos[2, :]))
|
|
@@ -551,12 +552,12 @@ def compute_pcd(delta_pos, delta_rot, skin_cortex_distance=20):
|
|
|
551
552
|
# rots[2] = 0
|
|
552
553
|
r = rot.from_rotvec(rots, degrees=True)
|
|
553
554
|
ray_dir = r.apply([0, 0, 1])
|
|
554
|
-
intersec = pynibs.intersection_vec_plan(ray_dir, ray_origin, plane_n, plane_p, eps=1e-6)
|
|
555
|
+
intersec = pynibs.util.utils.intersection_vec_plan(ray_dir, ray_origin, plane_n, plane_p, eps=1e-6)
|
|
555
556
|
|
|
556
557
|
# compute distance from new intersection with [0, 0, scd]
|
|
557
558
|
delta_rot_a.append(np.linalg.norm(plane_p - intersec))
|
|
558
559
|
|
|
559
560
|
# ray shift based on orientation doesn't include any rotations around z axis.
|
|
560
|
-
z_rotation_deltas = np.abs(
|
|
561
|
+
z_rotation_deltas = np.abs(skin_cortex_distance * np.sin(np.deg2rad(delta_rot[2, :])))
|
|
561
562
|
delta_rot = np.array(delta_rot_a) + z_rotation_deltas
|
|
562
563
|
return delta_pos_summed + delta_rot, delta_pos_summed, delta_rot
|