pyNIBS 0.2024.8__py3-none-any.whl → 0.2026.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (101) hide show
  1. pynibs/__init__.py +26 -14
  2. pynibs/coil/__init__.py +6 -0
  3. pynibs/{coil.py → coil/coil.py} +213 -543
  4. pynibs/coil/export.py +508 -0
  5. pynibs/congruence/__init__.py +4 -1
  6. pynibs/congruence/congruence.py +37 -45
  7. pynibs/congruence/ext_metrics.py +40 -11
  8. pynibs/congruence/stimulation_threshold.py +1 -2
  9. pynibs/expio/Mep.py +120 -370
  10. pynibs/expio/__init__.py +10 -0
  11. pynibs/expio/brainsight.py +34 -37
  12. pynibs/expio/cobot.py +25 -25
  13. pynibs/expio/exp.py +10 -7
  14. pynibs/expio/fit_funs.py +3 -0
  15. pynibs/expio/invesalius.py +70 -0
  16. pynibs/expio/localite.py +190 -91
  17. pynibs/expio/neurone.py +139 -0
  18. pynibs/expio/signal_ced.py +345 -2
  19. pynibs/expio/visor.py +16 -15
  20. pynibs/freesurfer.py +34 -33
  21. pynibs/hdf5_io/hdf5_io.py +149 -132
  22. pynibs/hdf5_io/xdmf.py +35 -31
  23. pynibs/mesh/__init__.py +1 -1
  24. pynibs/mesh/mesh_struct.py +77 -92
  25. pynibs/mesh/transformations.py +121 -21
  26. pynibs/mesh/utils.py +191 -99
  27. pynibs/models/_TMS.py +2 -1
  28. pynibs/muap.py +1 -2
  29. pynibs/neuron/__init__.py +10 -0
  30. pynibs/neuron/models/mep.py +566 -0
  31. pynibs/neuron/neuron_regression.py +98 -8
  32. pynibs/optimization/__init__.py +12 -2
  33. pynibs/optimization/{optimization.py → coil_opt.py} +157 -133
  34. pynibs/optimization/multichannel.py +1174 -24
  35. pynibs/optimization/workhorses.py +7 -8
  36. pynibs/regression/__init__.py +4 -2
  37. pynibs/regression/dual_node_detection.py +229 -219
  38. pynibs/regression/regression.py +92 -61
  39. pynibs/roi/__init__.py +4 -1
  40. pynibs/roi/roi_structs.py +19 -21
  41. pynibs/roi/{roi.py → roi_utils.py} +56 -33
  42. pynibs/subject.py +24 -14
  43. pynibs/util/__init__.py +20 -4
  44. pynibs/util/dosing.py +4 -5
  45. pynibs/util/quality_measures.py +39 -38
  46. pynibs/util/rotations.py +116 -9
  47. pynibs/util/{simnibs.py → simnibs_io.py} +29 -19
  48. pynibs/util/{util.py → utils.py} +20 -22
  49. pynibs/visualization/para.py +4 -4
  50. pynibs/visualization/render_3D.py +4 -4
  51. pynibs-0.2026.1.dist-info/METADATA +105 -0
  52. pynibs-0.2026.1.dist-info/RECORD +69 -0
  53. {pyNIBS-0.2024.8.dist-info → pynibs-0.2026.1.dist-info}/WHEEL +1 -1
  54. pyNIBS-0.2024.8.dist-info/METADATA +0 -723
  55. pyNIBS-0.2024.8.dist-info/RECORD +0 -107
  56. pynibs/data/configuration_exp0.yaml +0 -59
  57. pynibs/data/configuration_linear_MEP.yaml +0 -61
  58. pynibs/data/configuration_linear_RT.yaml +0 -61
  59. pynibs/data/configuration_sigmoid4.yaml +0 -68
  60. pynibs/data/network mapping configuration/configuration guide.md +0 -238
  61. pynibs/data/network mapping configuration/configuration_TEMPLATE.yaml +0 -42
  62. pynibs/data/network mapping configuration/configuration_for_testing.yaml +0 -43
  63. pynibs/data/network mapping configuration/configuration_modelTMS.yaml +0 -43
  64. pynibs/data/network mapping configuration/configuration_reg_isi_05.yaml +0 -43
  65. pynibs/data/network mapping configuration/output_documentation.md +0 -185
  66. pynibs/data/network mapping configuration/recommendations_for_accuracy_threshold.md +0 -77
  67. pynibs/data/neuron/models/L23_PC_cADpyr_biphasic_v1.csv +0 -1281
  68. pynibs/data/neuron/models/L23_PC_cADpyr_monophasic_v1.csv +0 -1281
  69. pynibs/data/neuron/models/L4_LBC_biphasic_v1.csv +0 -1281
  70. pynibs/data/neuron/models/L4_LBC_monophasic_v1.csv +0 -1281
  71. pynibs/data/neuron/models/L4_NBC_biphasic_v1.csv +0 -1281
  72. pynibs/data/neuron/models/L4_NBC_monophasic_v1.csv +0 -1281
  73. pynibs/data/neuron/models/L4_SBC_biphasic_v1.csv +0 -1281
  74. pynibs/data/neuron/models/L4_SBC_monophasic_v1.csv +0 -1281
  75. pynibs/data/neuron/models/L5_TTPC2_cADpyr_biphasic_v1.csv +0 -1281
  76. pynibs/data/neuron/models/L5_TTPC2_cADpyr_monophasic_v1.csv +0 -1281
  77. pynibs/tests/data/InstrumentMarker20200225163611937.xml +0 -19
  78. pynibs/tests/data/TriggerMarkers_Coil0_20200225163443682.xml +0 -14
  79. pynibs/tests/data/TriggerMarkers_Coil1_20200225170337572.xml +0 -6373
  80. pynibs/tests/data/Xdmf.dtd +0 -89
  81. pynibs/tests/data/brainsight_niiImage_nifticoord.txt +0 -145
  82. pynibs/tests/data/brainsight_niiImage_nifticoord_largefile.txt +0 -1434
  83. pynibs/tests/data/brainsight_niiImage_niifticoord_mixedtargets.txt +0 -47
  84. pynibs/tests/data/create_subject_testsub.py +0 -332
  85. pynibs/tests/data/data.hdf5 +0 -0
  86. pynibs/tests/data/geo.hdf5 +0 -0
  87. pynibs/tests/test_coil.py +0 -474
  88. pynibs/tests/test_elements2nodes.py +0 -100
  89. pynibs/tests/test_hdf5_io/test_xdmf.py +0 -61
  90. pynibs/tests/test_mesh_transformations.py +0 -123
  91. pynibs/tests/test_mesh_utils.py +0 -143
  92. pynibs/tests/test_nnav_imports.py +0 -101
  93. pynibs/tests/test_quality_measures.py +0 -117
  94. pynibs/tests/test_regressdata.py +0 -289
  95. pynibs/tests/test_roi.py +0 -17
  96. pynibs/tests/test_rotations.py +0 -86
  97. pynibs/tests/test_subject.py +0 -71
  98. pynibs/tests/test_util.py +0 -24
  99. /pynibs/{regression/score_types.py → neuron/models/m1_montbrio.py} +0 -0
  100. {pyNIBS-0.2024.8.dist-info → pynibs-0.2026.1.dist-info/licenses}/LICENSE +0 -0
  101. {pyNIBS-0.2024.8.dist-info → pynibs-0.2026.1.dist-info}/top_level.txt +0 -0
pynibs/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.arrays
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
- elms_with_island : list
859
- Elements with neighboring islands
860
- counter_visited : np.ndarray
861
- shape = (n_elms). How often as each element been visited.
862
- counter_not_visited : np.ndarray
863
- shape = (n_elms). How often as each element not been visited.
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 defines as a set of elements, which share a single node and/or single edge with the rest of the mesh.
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, write_xdmf=True, skin_surface_id=1005, verbose=False):
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 = pynibs.find_islands(connectivity=tri_nodes,
1021
- verbose=True,
1022
- island_crit='edge',
1023
- largest=True)
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=hdf5.triangles_center)
1037
- if verbose:
1038
- print("Computing tetrahedra")
1039
-
1040
- distances_tets = np.apply_along_axis(fun, axis=1, arr=hdf5.tetrahedra_center)
1041
- with h5py.File(mesh_fn, 'a') as f:
1042
- try:
1043
- del f['data/tris/Cortex_dist']
1044
- del f['data/tets/Cortex_dist']
1045
- except KeyError:
1046
- pass
1047
- f.create_dataset(name='data/tris/Cortex_dist', data=distances_tri)
1048
- f.create_dataset(name='data/tets/Cortex_dist', data=distances_tets)
1049
-
1050
- if write_xdmf:
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.simnibs.read_coil_geo(fn_coil_geo)
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 *