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
@@ -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 *_ROIs
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
- Example
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.gifti.giftiio.read(os.path.join(mesh_folder, gm_surf_fname[i]))
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.gifti.giftiio.read(os.path.join(mesh_folder, wm_surf_fname[i]))
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.gifti.giftiio.read(os.path.join(mesh_folder, midlayer_surf_fname[i]))
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
- os.path.join(mesh_folder, midlayer_surf_fname[i]))
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
- fn_surf_refined=roi_refined_fn,
593
- center=[0, 0, 0],
594
- radius=np.inf,
595
- verbose=True,
596
- repair=False,
597
- remesh=False)
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
- fn_surf_refined=roi_refined_fn,
613
- center=[0, 0, 0],
614
- radius=np.inf,
615
- verbose=True,
616
- repair=False)
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
- center=center,
780
- radius=radius,
781
- nii=final_tissues_nii,
782
- val_in=target_size,
783
- out_fn=out_spher_fn,
784
- outside_val=outside_size, outside_radius=radius * outside_factor)
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
- .msh or .hdf5 file containing the mesh information.
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. dicretization, etc.).
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 : nparray
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 : nparray of float
776
+ clip_coords : np.ndarray of float
776
777
  (N_clips, 3) Coordinates of clip surface origins (x,y,z).
777
- clip_normals : nparray of float
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
- + 'scaled': size scaled according to dipole magnitude
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 nparray
818
- If nparray [1 x 4*N]: custom colormap providing data and corresponding RGB values
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
- + 'BuRd'
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 : nparray [1 x 2]
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 magnitudeeee
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
- from .util import *
2
- from .quality_measures import *
3
- from .rotations import *
4
- from .simnibs import *
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)
@@ -1,8 +1,14 @@
1
- import numpy as np
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
- from matplotlib import pyplot as plt
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
- Example
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 euclidean distance of all nodes to source node (triangle).
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
- Example
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
- nrmse = np.linalg.norm(data - data_ref, axis=0) / np.linalg.norm(data_ref, axis=0)
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
- Rorational changes are quantified with respect to the coil axes as follows:
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/roation, for example
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
- Example
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(pos_diff_rel, euler_rots_rel, pcd)
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((skin_cortex_distance) * np.sin(np.deg2rad(delta_rot[2, :])))
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