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,18 +1,15 @@
1
- """
2
- All functions to operate on TMS coils go here, for example to create ``.xdmf`` files to visualize coil positions.
3
- """
4
1
  import os
2
+ import tqdm
5
3
  import copy
6
4
  import math
7
5
  import h5py
8
- import shutil
9
6
  import random
7
+ import shutil
10
8
  import itertools
11
- import pandas as pd
12
9
  import numpy as np
13
- import matplotlib.pyplot as plt
14
10
  from scipy.spatial import Delaunay
15
11
  from collections import OrderedDict
12
+ from matplotlib import pyplot as plt
16
13
 
17
14
  import pynibs
18
15
 
@@ -59,7 +56,7 @@ def check_coil_position(points, hull):
59
56
  valid : bool
60
57
  Validity of coil position:
61
58
  TRUE: valid
62
- FALSE: unvalid
59
+ FALSE: invalid
63
60
  """
64
61
  # make Delaunay grid if not already passed
65
62
  if not isinstance(hull, Delaunay):
@@ -117,7 +114,7 @@ def calc_coil_transformation_matrix(LOC_mean, ORI_mean, LOC_var, ORI_var, V):
117
114
  \\end{bmatrix}
118
115
  """
119
116
  # calculate rotation matrix for angle variation (angle_var in deg)
120
- rotation_matrix = pynibs.euler_angles_to_rotation_matrix(ORI_var * np.pi / 180.)
117
+ rotation_matrix = pynibs.util.rotations.euler_angles_to_rotation_matrix(ORI_var * np.pi / 180.)
121
118
 
122
119
  # determine new orientation
123
120
  ori = np.dot(ORI_mean, rotation_matrix)
@@ -196,7 +193,7 @@ def calc_coil_position_pdf(fn_rescon=None, fn_simpos=None, fn_exp=None, orientat
196
193
  folder_pdfplots = ''
197
194
 
198
195
  if fn_rescon and fn_simpos:
199
- positions_all, conditions, position_list, _, _ = pynibs.read_exp_stimulations(fn_rescon, fn_simpos)
196
+ positions_all, conditions, position_list, _, _ = pynibs.expio.read_exp_stimulations(fn_rescon, fn_simpos)
200
197
 
201
198
  # sort POSITIONS according to CONDITIONS, idx_con is alphabetically sorted, first index of condition[*]
202
199
  # appeareance
@@ -382,7 +379,8 @@ def test_coil_position_gpc(parameters):
382
379
  triangles = node_number_list[elm_type == 2, 0:3]
383
380
 
384
381
  triangles = triangles[triangles_regions == 5]
385
- surface_points = pynibs.unique_rows(np.reshape(points[triangles], (3 * triangles.shape[0], 3)))
382
+ surface_points = pynibs.util.utils.unique_rows(np.reshape(points[triangles],
383
+ (3 * triangles.shape[0], 3)))
386
384
  limits_scaling_factor = .1
387
385
 
388
386
  # Generate Delaunay triangulation object
@@ -501,12 +499,12 @@ def get_invalid_coil_parameters(param_dict, coil_position_mean, svd_v, del_obj,
501
499
  limits_theta = param_dict['theta']['limits']
502
500
  limits_phi = param_dict['phi']['limits']
503
501
 
504
- limits_pos_x = pynibs.add_center(limits_pos_x)
505
- limits_pos_y = pynibs.add_center(limits_pos_y)
506
- limits_pos_z = pynibs.add_center(limits_pos_z)
507
- limits_psi = pynibs.add_center(limits_psi)
508
- limits_theta = pynibs.add_center(limits_theta)
509
- limits_phi = pynibs.add_center(limits_phi)
502
+ limits_pos_x = pynibs.util.utils.add_center(limits_pos_x)
503
+ limits_pos_y = pynibs.util.utils.add_center(limits_pos_y)
504
+ limits_pos_z = pynibs.util.utils.add_center(limits_pos_z)
505
+ limits_psi = pynibs.util.utils.add_center(limits_psi)
506
+ limits_theta = pynibs.util.utils.add_center(limits_theta)
507
+ limits_phi = pynibs.util.utils.add_center(limits_phi)
510
508
 
511
509
  temp_list = [[0, 1, 2], [0, 1, 2], [0, 1, 2], [0, 1, 2], [0, 1, 2], [0, 1, 2]]
512
510
  combinations = list(itertools.product(*temp_list))
@@ -582,513 +580,10 @@ def get_invalid_coil_parameters(param_dict, coil_position_mean, svd_v, del_obj,
582
580
  return fail_params
583
581
 
584
582
 
585
- def create_stimsite_hdf5(fn_exp, fn_hdf, conditions_selected=None, sep="_", merge_sites=False, fix_angles=False,
586
- data_dict=None, conditions_ignored=None):
587
- """
588
- Reads results_conditions and creates an hdf5/xdmf pair with condition-wise centers of stimulation sites and
589
- coil directions as data.
590
-
591
- Parameters
592
- ----------
593
- fn_exp : str
594
- Path to results.csv.
595
- fn_hdf : str
596
- Path where to write file. Gets overridden if already existing.
597
- conditions_selected : str or list of str, optional
598
- List of conditions returned by the function, the others are omitted.
599
- If None, all conditions are returned.
600
- sep: str, default: "_"
601
- Separator between condition label and angle (e.g. M1_0, or M1-0).
602
- merge_sites : bool
603
- If true, only one coil center per site is generated.
604
- fix_angles : bool
605
- rename 22.5 -> 0, 0 -> -45, 67.5 -> 90, 90 -> 135.
606
- data_dict : dict ofnp.ndarray of float [n_stimsites] (optional), default: None
607
- Dictionary containing data corresponding to the stimulation sites (keys).
608
- conditions_ignored : str or list of str, optional
609
- Conditions, which are not going to be included in the plot.
610
-
611
- Returns
612
- -------
613
- <Files> : hdf5/xdmf file pair
614
- Contains information about condition-wise stimulation sites and coil directions (fn_hdf)
615
-
616
- Example
617
- -------
618
- .. code-block:: python
619
-
620
- pynibs.create_stimsite_hdf5('/exp/1/experiment_corrected.csv',
621
- '/stimsite', True, True)
622
- """
623
- assert not fn_hdf.endswith('/')
624
-
625
- exp = pynibs.read_csv(fn_exp)
626
-
627
- exp_cond = pynibs.sort_by_condition(exp, conditions_selected=conditions_selected) # []
628
-
629
- # get the unique conditions in the correct order
630
- conds = [c['condition'][0] for c in exp_cond]
631
-
632
- # remove conds
633
- conds_temp = []
634
- exp_cond_temp = []
635
-
636
- if type(conditions_ignored) is not list:
637
- conditions_ignored = [conditions_ignored]
638
-
639
- for i_c, c in enumerate(conds):
640
- ignore = False
641
- for ci in list(conditions_ignored):
642
- if c == ci:
643
- ignore = True
644
-
645
- if not ignore:
646
- conds_temp.append(conds[i_c])
647
- exp_cond_temp.append(exp_cond[i_c])
648
-
649
- exp_cond = exp_cond_temp
650
- conds = conds_temp
651
-
652
- # hardcoded row #3 is condition
653
- cond_idx = np.linspace(0, len(exp_cond), 1)[:, np.newaxis]
654
-
655
- centers = []
656
- m0 = []
657
- m1 = []
658
- m2 = []
659
-
660
- for i_cond in range(len(exp_cond)):
661
- centers.append(exp_cond[i_cond]['coil_mean_matrix'][0][0:3, 3])
662
- m0.append(exp_cond[i_cond]['coil_mean_matrix'][0][0:3, 0])
663
- m1.append(exp_cond[i_cond]['coil_mean_matrix'][0][0:3, 1])
664
- m2.append(exp_cond[i_cond]['coil_mean_matrix'][0][0:3, 2])
665
-
666
- # split conds to angles and sites: M1_90 -> M1, 90
667
- angles = np.array([sp.split(sep)[-1] for sp in conds]).astype(np.float64)
668
- sites = np.array([sp.split(sep)[0] for sp in conds])
669
- sites_unique = np.unique(sites)
670
-
671
- # average the center positions of a stimulation site over all orientations
672
- if merge_sites:
673
-
674
- # generate sites dict
675
- centers_sites = dict()
676
-
677
- for site in sites_unique:
678
- centers_sites[site] = []
679
-
680
- # gather all orientations and put them to the corresponding sites
681
- for i_cond, site in enumerate(sites):
682
- centers_sites[site].append(exp_cond[i_cond]['coil_mean_matrix'][0][0:3, 3])
683
-
684
- # determine average position over all orientations for each site
685
- for site in sites_unique:
686
- centers_sites[site] = np.mean(np.vstack(centers_sites[site]), axis=0)
687
-
688
- # write it back to centers
689
- for i_cond, site in enumerate(sites):
690
- centers[i_cond] = centers_sites[site]
691
-
692
- centers = np.vstack(centers)
693
- m0 = np.vstack(m0)
694
- m1 = np.vstack(m1)
695
- m2 = np.vstack(m2)
696
-
697
- # enumerate sites, as paraview does not plot string array data
698
- sites_idx = np.array(list(range(len(sites))))[:, np.newaxis]
699
-
700
- angles[angles[:] == 675.] = 67.5
701
- angles[angles[:] == 225.] = 22.5
702
-
703
- if fix_angles:
704
- # rename wrong angle names
705
- angles_cor = np.copy(angles)
706
- angles_cor[angles == 0] = -45.
707
- angles_cor[angles == 22.5] = 0.
708
- angles_cor[angles == 67.5] = 90.
709
- angles_cor[angles == 90] = 135.
710
- angles = angles_cor
711
-
712
- # write hdf5 file
713
- if not fn_hdf.endswith('.hdf5'):
714
- fn_hdf += '.hdf5'
715
- f = h5py.File(fn_hdf, 'w')
716
- f.create_dataset('centers', data=centers.astype(np.float64))
717
- f.create_dataset('m0', data=m0.astype(np.float64))
718
- f.create_dataset('m1', data=m1.astype(np.float64))
719
- f.create_dataset('m2', data=m2.astype(np.float64))
720
- f.create_dataset('cond', data=np.string_(conds)) # this is a string array, not xdmf compatible
721
- f.create_dataset('cond_idx', data=cond_idx)
722
- f.create_dataset('angles', data=angles)
723
- f.create_dataset('sites', data=np.string_(sites)) # this is a string array, not xdmf compatible
724
- f.create_dataset('sites_idx', data=sites_idx)
725
-
726
- data = None
727
- if data_dict is not None:
728
- data = np.zeros((len(list(data_dict.keys())), 1))
729
- for i_data, cond in enumerate(conds):
730
- data[i_data, 0] = data_dict[cond]
731
- f.create_dataset('data', data=data)
732
- f.close()
733
-
734
- # write .xdmf file
735
- f = open(fn_hdf[:-4] + 'xdmf', 'w')
736
- fn_hdf = os.path.basename(fn_hdf) # relative links
737
-
738
- # header
739
- f.write('<?xml version="1.0"?>\n')
740
- f.write('<!DOCTYPE Xdmf SYSTEM "Xdmf.dtd" []>\n')
741
- f.write('<Xdmf Version="2.0" xmlns:xi="http://www.w3.org/2001/XInclude">\n')
742
- f.write('<Domain>\n')
743
- f.write('<Grid\nCollectionType="Spatial"\nGridType="Collection"\nName="Collection">\n')
744
-
745
- # one grid for coil dipole nodes...store data hdf5.
746
- #######################################################
747
- f.write('<Grid Name="stimsites" GridType="Uniform">\n')
748
- f.write('<Topology NumberOfElements="' + str(centers.shape[0]) +
749
- '" TopologyType="Polyvertex" Name="Tri">\n')
750
- f.write('<DataItem Format="XML" Dimensions="' + str(centers.shape[0]) + ' 1">\n')
751
- # f.write(hdf5_fn + ':' + path + '/triangle_number_list\n')
752
- np.savetxt(f, list(range(centers.shape[0])), fmt='%d', delimiter=' ') # 1 2 3 4 ... N_Points
753
- f.write('</DataItem>\n')
754
- f.write('</Topology>\n')
755
-
756
- # nodes
757
- f.write('<Geometry GeometryType="XYZ">\n')
758
- f.write('<DataItem Format="HDF" Dimensions="' + str(centers.shape[0]) + ' 3">\n')
759
- f.write(fn_hdf + ':' + '/centers\n')
760
- f.write('</DataItem>\n')
761
- f.write('</Geometry>\n')
762
-
763
- # data
764
- # dipole magnitude
765
- # the 4 vectors
766
- for i in range(3):
767
- f.write('<Attribute Name="dir_' + str(i) + '" AttributeType="Vector" Center="Cell">\n')
768
- f.write('<DataItem Format="HDF" Dimensions="' + str(centers.shape[0]) + ' 3">\n')
769
- f.write(fn_hdf + ':' + '/m' + str(i) + '\n')
770
- f.write('</DataItem>\n')
771
- f.write('</Attribute>\n\n')
772
-
773
- # angles
774
- f.write('<Attribute Name="angles" AttributeType="Scalar" Center="Cell">\n')
775
- f.write('<DataItem Format="HDF" Dimensions="' + str(centers.shape[0]) + ' 1">\n')
776
- f.write(fn_hdf + ':' + '/angles\n')
777
- f.write('</DataItem>\n')
778
- f.write('</Attribute>\n\n')
779
-
780
- # data
781
- if data_dict is not None:
782
- f.write('<Attribute Name="data" AttributeType="Scalar" Center="Cell">\n')
783
- f.write('<DataItem Format="HDF" Dimensions="' + str(data.shape[0]) + ' 1">\n')
784
- f.write(fn_hdf + ':' + '/data\n')
785
- f.write('</DataItem>\n')
786
- f.write('</Attribute>\n\n')
787
-
788
- # site idx
789
- f.write('<Attribute Name="sites_idx" AttributeType="Scalar" Center="Cell">\n')
790
- f.write('<DataItem Format="HDF" Dimensions="' + str(centers.shape[0]) + ' 1">\n')
791
- f.write(fn_hdf + ':' + '/sites_idx\n')
792
- f.write('</DataItem>\n')
793
- f.write('</Attribute>\n\n')
794
-
795
- f.write('</Grid>\n')
796
- # end coil dipole data
797
-
798
- # footer
799
- f.write('</Grid>\n')
800
- f.write('</Domain>\n')
801
- f.write('</Xdmf>\n')
802
- f.close()
803
-
804
-
805
- def create_stimsite_from_list(fn_hdf, poslist, datanames=None, data=None, overwrite=False):
806
- """
807
- This takes a list of matsimnibs-style coil position and orientations and creates an .hdf5 + .xdmf tuple
808
- for all positions.
809
-
810
- Centers and coil orientations are written to disk, with optional data for each coil configuration.
811
-
812
- Parameters
813
- ----------
814
- fn_hdf: str
815
- Filename for the .hdf5 file. The .xdmf is saved with the same basename.
816
- Folder should already exist.
817
- poslist: list of np.ndarray
818
- (4,4) Positions.
819
- datanames: str or list of str, optional
820
- Dataset names for ``data``.
821
- data: np.ndarray, optional
822
- Dataset array with shape = ``(len(poslist.pos), len(datanames())``.
823
- overwrite : bool, defaul: False
824
- Overwrite existing files.
825
- """
826
- centers = []
827
- m0 = []
828
- m1 = []
829
- m2 = []
830
- if data is not None:
831
- assert isinstance(data, np.ndarray)
832
-
833
- for lst in poslist:
834
- centers.append(lst[0:3, 3])
835
- m0.append(lst[0:3, 0])
836
- m1.append(lst[0:3, 1])
837
- m2.append(lst[0:3, 2])
838
-
839
- centers = np.vstack(centers)
840
- m0 = np.vstack(m0)
841
- m1 = np.vstack(m1)
842
- m2 = np.vstack(m2)
843
-
844
- write_coil_pos_hdf5(fn_hdf, centers, m0, m1, m2, datanames=datanames, data=data, overwrite=overwrite)
845
-
846
-
847
- def create_stimsite_from_tmslist(fn_hdf, poslist, datanames=None, data=None, overwrite=False):
848
- """
849
- This takes a :py:class:simnibs.sim_struct.TMSLIST from simnibs and creates an .hdf5 + .xdmf tuple for all positions.
850
-
851
- Centers and coil orientations are written to disk, with optional data for each coil configuration.
852
-
853
- Parameters
854
- ----------
855
- fn_hdf: str
856
- Filename for the .hdf5 file. The .xdmf is saved with the same basename.
857
- Folder should already exist.
858
- poslist: simnibs.sim_struct.TMSLIST
859
- poslist.pos[*].matsimnibs have to be set.
860
- datanames: str or list of str, optional
861
- Dataset names for ``data``.
862
- data: np.ndarray, optional
863
- Dataset array with shape = ``(len(poslist.pos), len(datanames())``.
864
- overwrite : bool, default: False
865
- Overwrite existing files
866
- """
867
- centers = []
868
- m0 = []
869
- m1 = []
870
- m2 = []
871
- assert poslist.pos
872
- if data is not None:
873
- assert isinstance(data, np.ndarray)
874
- for pos in poslist.pos:
875
- assert pos.matsimnibs is not None
876
- pos.matsimnibs = np.array(pos.matsimnibs)
877
- centers.append(pos.matsimnibs[0:3, 3])
878
- m0.append(pos.matsimnibs[0:3, 0])
879
- m1.append(pos.matsimnibs[0:3, 1])
880
- m2.append(pos.matsimnibs[0:3, 2])
881
-
882
- centers = np.vstack(centers)
883
- m0 = np.vstack(m0)
884
- m1 = np.vstack(m1)
885
- m2 = np.vstack(m2)
886
-
887
- write_coil_pos_hdf5(fn_hdf, centers, m0, m1, m2, datanames=datanames, data=data, overwrite=overwrite)
888
-
889
-
890
- def create_stimsite_from_exp_hdf5(fn_exp, fn_hdf, datanames=None, data=None, overwrite=False):
891
- """
892
- This takes an experiment.hdf5 file and creates an .hdf5 + .xdmf tuple for all coil positions for visualization.
893
-
894
- Parameters
895
- ----------
896
- fn_exp : str
897
- Path to experiment.hdf5
898
- fn_hdf : str
899
- Filename for the resulting .hdf5 file. The .xdmf is saved with the same basename.
900
- Folder should already exist.
901
- datanames : str or list of str, optional
902
- Dataset names for ``data``
903
- data : np.ndarray, optional
904
- Dataset array with shape = ``(len(poslist.pos), len(datanames())``.
905
- overwrite : bool, default: False
906
- Overwrite existing files.
907
- """
908
- df_stim = pd.read_hdf(fn_exp, "stim_data")
909
-
910
- matsimnibs = np.zeros((4, 4, df_stim.shape[0]))
911
-
912
- for i in range(df_stim.shape[0]):
913
- matsimnibs[:, :, i] = df_stim["coil_mean"].iloc[i]
914
-
915
- create_stimsite_from_matsimnibs(fn_hdf=fn_hdf,
916
- matsimnibs=matsimnibs,
917
- datanames=datanames,
918
- data=data,
919
- overwrite=overwrite)
920
-
921
-
922
- def create_stimsite_from_matsimnibs(fn_hdf, matsimnibs, datanames=None, data=None, overwrite=False):
923
- """
924
- This takes a matsimnibs array and creates an .hdf5 + .xdmf tuple for all coil positions for visualization.
925
-
926
- Centers and coil orientations are written disk.
927
-
928
- Parameters
929
- ----------
930
- fn_hdf: str
931
- Filename for the .hdf5 file. The .xdmf is saved with the same basename.
932
- Folder should already exist.
933
- matsimnibs: np.ndarray
934
- (4, 4, n_pos)
935
- Matsimnibs matrices containing the coil orientation (x,y,z) and position (p)
936
-
937
- .. math::
938
- \\begin{bmatrix}
939
- | & | & | & | \\\\
940
- x & y & z & p \\\\
941
- | & | & | & | \\\\
942
- 0 & 0 & 0 & 1 \\\\
943
- \\end{bmatrix}
944
- datanames: str or list of str, optional
945
- Dataset names for ``data``.
946
- data: np.ndarray, optional
947
- (len(poslist.pos), len(datanames).
948
- overwrite : bool, default: False
949
- Overwrite existing files.
950
- """
951
- matsimnibs = np.atleast_3d(matsimnibs)
952
- n_pos = matsimnibs.shape[2]
953
- centers = np.zeros((n_pos, 3))
954
- m0 = np.zeros((n_pos, 3))
955
- m1 = np.zeros((n_pos, 3))
956
- m2 = np.zeros((n_pos, 3))
957
- if data is not None:
958
- assert isinstance(data, np.ndarray)
959
-
960
- for i in range(matsimnibs.shape[2]):
961
- centers[i, :] = matsimnibs[0:3, 3, i]
962
- m0[i, :] = matsimnibs[0:3, 0, i]
963
- m1[i, :] = matsimnibs[0:3, 1, i]
964
- m2[i, :] = matsimnibs[0:3, 2, i]
965
-
966
- write_coil_pos_hdf5(fn_hdf, centers, m0, m1, m2, datanames=datanames, data=data, overwrite=overwrite)
967
-
968
-
969
- def write_coil_pos_hdf5(fn_hdf, centers, m0, m1, m2, datanames=None, data=None, overwrite=False):
970
- """
971
- Creates a ``.hdf5`` + ``.xdmf`` file tuple for all coil positions.
972
- Coil centers and coil orientations are saved, and - optionally - data for each position if ``data`` and
973
- ``datanames`` are provided.
974
-
975
- Parameters
976
- ----------
977
- fn_hdf : str
978
- Filename for the .hdf5 file. The .xdmf is saved with the same basename.
979
- Folder should already exist.
980
- centers : np.ndarray of float
981
- (n_pos, 3) Coil positions.
982
- m0 : np.ndarray of float
983
- (n_pos, 3) Coil orientation x-axis (looking at the active (patient) side of the coil pointing to the right).
984
- m1 : np.ndarray of float
985
- (n_pos, 3) Coil orientation y-axis (looking at the active side of the coil pointing up away from the handle).
986
- m2 : np.ndarray of float
987
- (n_pos, 3) Coil orientation z-axis (looking at the active (patient) side of the coil pointing to the patient).
988
- datanames : str or list of str, optional
989
- (n_data) Dataset names for ``data``
990
- data : np.ndarray, optional
991
- (n_pos, n_data) Dataset array with (len(poslist.pos), len(datanames()).
992
- overwrite : bool, default: False
993
- Overwrite existing files.
994
- """
995
- n_pos = centers.shape[0]
996
- if isinstance(datanames, str):
997
- datanames = [datanames]
998
-
999
- if data is not None:
1000
- if datanames is None:
1001
- raise ValueError("Provide datanames= with data= argument.")
1002
- if isinstance(datanames, str):
1003
- datanames = [datanames]
1004
- if len(data.shape) <= 1:
1005
- data = np.atleast_1d(data)[:, np.newaxis]
1006
- assert data.shape == (n_pos, len(datanames))
1007
- if datanames is not None and data is None:
1008
- raise ValueError("Provide data= with datanames= argument.")
1009
-
1010
- m0_reshaped = np.hstack((m0, np.zeros((n_pos, 1)))).T[:, np.newaxis, :]
1011
- m1_reshaped = np.hstack((m1, np.zeros((n_pos, 1)))).T[:, np.newaxis, :]
1012
- m2_reshaped = np.hstack((m2, np.zeros((n_pos, 1)))).T[:, np.newaxis, :]
1013
- centers_reshaped = np.hstack((centers, np.ones((n_pos, 1)))).T[:, np.newaxis, :]
1014
-
1015
- matsimnibs = np.concatenate((m0_reshaped, m1_reshaped, m2_reshaped, centers_reshaped), axis=1)
1016
-
1017
- # write hdf5 file
1018
- if not fn_hdf.endswith('.hdf5'):
1019
- fn_hdf += '.hdf5'
1020
- if os.path.exists(fn_hdf) and not overwrite:
1021
- raise OSError(fn_hdf + " already exists. Set overwrite flag for create_stimsite_from_poslist.")
1022
-
1023
- with h5py.File(fn_hdf, 'w') as f:
1024
- f.create_dataset('centers', data=centers.astype(np.float64))
1025
- f.create_dataset('m0', data=m0.astype(np.float64))
1026
- f.create_dataset('m1', data=m1.astype(np.float64))
1027
- f.create_dataset('m2', data=m2.astype(np.float64))
1028
- f.create_dataset("matsimnibs", data=matsimnibs)
1029
-
1030
- if data is not None:
1031
- for i, col in enumerate(data.T):
1032
- f.create_dataset('/data/' + datanames[i], data=col)
1033
-
1034
- # write .xdmf file
1035
- with open(fn_hdf[:-4] + 'xdmf', 'w') as f:
1036
- fn_hdf = os.path.basename(fn_hdf) # relative links
1037
-
1038
- # header
1039
- f.write('<?xml version="1.0"?>\n')
1040
- f.write('<!DOCTYPE Xdmf SYSTEM "Xdmf.dtd" []>\n')
1041
- f.write('<Xdmf Version="2.0" xmlns:xi="http://www.w3.org/2001/XInclude">\n')
1042
- f.write('<Domain>\n')
1043
- f.write('<Grid CollectionType="Spatial" GridType="Collection" Name="Collection">\n')
1044
-
1045
- # one grid for coil dipole nodes...store data hdf5.
1046
- #######################################################
1047
- f.write('<Grid Name="stimsites" GridType="Uniform">\n')
1048
- f.write(f'<Topology NumberOfElements="{centers.shape[0]}" TopologyType="Polyvertex" Name="Tri">\n')
1049
- f.write(f'\t<DataItem Format="XML" Dimensions="{centers.shape[0]} 1">\n')
1050
- np.savetxt(f, list(range(centers.shape[0])), fmt='\t%d', delimiter=' ') # 1 2 3 4 ... N_Points
1051
- f.write('\t</DataItem>\n')
1052
- f.write('</Topology>\n\n')
1053
-
1054
- # nodes
1055
- f.write('<Geometry GeometryType="XYZ">\n')
1056
- f.write(f'\t<DataItem Format="HDF" Dimensions="{centers.shape[0]} 3">\n')
1057
- f.write(f'\t{fn_hdf}:/centers\n')
1058
- f.write('\t</DataItem>\n')
1059
- f.write('</Geometry>\n\n')
1060
-
1061
- # data
1062
- # dipole magnitude
1063
- # the 4 vectors
1064
- for i in range(3):
1065
- f.write(f'\t\t<Attribute Name="dir_{i}" AttributeType="Vector" Center="Cell">\n')
1066
- f.write(f'\t\t\t<DataItem Format="HDF" Dimensions="{centers.shape[0]} 3">\n')
1067
- f.write(f'\t\t\t{fn_hdf}:/m{i}\n')
1068
- f.write('\t\t\t</DataItem>\n')
1069
- f.write('\t\t</Attribute>\n\n')
1070
-
1071
- if data is not None:
1072
- for i, col in enumerate(data.T):
1073
- f.write(f'\t\t<Attribute Name="{datanames[i]}" AttributeType="Scalar" Center="Cell">\n')
1074
- f.write('\t\t\t<DataItem Format="HDF" Dimensions="' + str(centers.shape[0]) + ' 1">\n')
1075
- f.write(f'\t\t\t{fn_hdf}:/data/{datanames[i]}\n')
1076
- f.write('\t\t\t</DataItem>\n')
1077
- f.write('\t\t</Attribute>\n\n')
1078
-
1079
- f.write('</Grid>\n')
1080
- # end coil dipole data
1081
-
1082
- # footer
1083
- f.write('</Grid>\n')
1084
- f.write('</Domain>\n')
1085
- f.write('</Xdmf>\n')
1086
-
1087
-
1088
583
  def sort_opt_coil_positions(fn_coil_pos_opt, fn_coil_pos, fn_out_hdf5=None, root_path="/0/0/", verbose=False,
1089
584
  print_output=False):
1090
585
  """
1091
- Sorts coil positions according to Traveling Salesman problem
586
+ Sorts coil positions according to Traveling Salesman problem.
1092
587
 
1093
588
  Parameters
1094
589
  ----------
@@ -1185,8 +680,8 @@ def sort_opt_coil_positions(fn_coil_pos_opt, fn_coil_pos, fn_out_hdf5=None, root
1185
680
  else:
1186
681
  # Euclidean distance
1187
682
  distances[from_counter][to_counter] = (int(
1188
- math.hypot((from_node[0] - to_node[0]),
1189
- (from_node[1] - to_node[1]))))
683
+ math.hypot((from_node[0] - to_node[0]),
684
+ (from_node[1] - to_node[1]))))
1190
685
  return distances
1191
686
 
1192
687
  def get_routes(s, r, m):
@@ -1293,7 +788,7 @@ def random_walk_coil(start_mat, n_steps, fn_mesh_hdf5, angles_dev=3, distance_lo
1293
788
 
1294
789
  Parameters
1295
790
  ----------
1296
- start_mat : np.ndarry
791
+ start_mat : np.ndarray
1297
792
  (4, 4) SimNIBS matsimnibs.
1298
793
  n_steps : int
1299
794
  Number of steps to walk.
@@ -1332,10 +827,10 @@ def random_walk_coil(start_mat, n_steps, fn_mesh_hdf5, angles_dev=3, distance_lo
1332
827
  # walk angles
1333
828
  for idx, axis in enumerate(['x', 'y', 'z']):
1334
829
  if angles_dev[idx] != 0:
1335
- mat_rot = pynibs.rotate_matsimnibs_euler(axis=axis,
1336
- angle=np.random.normal(scale=angles_dev[idx]),
1337
- matsimnibs=mat_rot,
1338
- metric=angles_metric)
830
+ mat_rot = pynibs.util.rotations.rotate_matsimnibs_euler(axis=axis,
831
+ angle=np.random.normal(scale=angles_dev[idx]),
832
+ matsimnibs=mat_rot,
833
+ metric=angles_metric)
1339
834
  mat[:3, :3] = mat_rot[:3, :3]
1340
835
  mats.append(mat)
1341
836
 
@@ -1345,23 +840,198 @@ def random_walk_coil(start_mat, n_steps, fn_mesh_hdf5, angles_dev=3, distance_lo
1345
840
  assert distance_low >= 0 and distance_high >= 0
1346
841
  assert distance_high >= distance_low
1347
842
  distances = np.random.uniform(low=distance_low, high=distance_high, size=n_steps + 1)
1348
- mats = pynibs.coil_distance_correction_matsimnibs(matsimnibs=mats,
1349
- fn_mesh_hdf5=fn_mesh_hdf5,
1350
- distance=distances)
843
+ mats = pynibs.expio.coil_distance_correction_matsimnibs(matsimnibs=mats,
844
+ fn_mesh_hdf5=fn_mesh_hdf5,
845
+ distance=distances)
1351
846
 
1352
847
  if coil_pos_fn is not None:
1353
- pynibs.write_coil_pos_hdf5(fn_hdf=os.path.splitext(coil_pos_fn)[0],
1354
- centers=mats[:3, 3, :].T,
1355
- m0=mats[:3, 0, :].T,
1356
- m1=mats[:3, 1, :].T,
1357
- m2=mats[:3, 2, :].T,
1358
- overwrite=True)
1359
-
1360
- pynibs.write_coil_sequence_xdmf(coil_pos_fn,
1361
- np.arange(n_steps + 1),
1362
- vec1=mats[:3, 0, :].T,
1363
- vec2=mats[:3, 1, :].T,
1364
- vec3=mats[:3, 2, :].T,
1365
- output_xdmf=f"{os.path.splitext(coil_pos_fn)[0]}.xdmf")
848
+ pynibs.coil.write_coil_pos_hdf5(fn_hdf=os.path.splitext(coil_pos_fn)[0],
849
+ centers=mats[:3, 3, :].T,
850
+ m0=mats[:3, 0, :].T,
851
+ m1=mats[:3, 1, :].T,
852
+ m2=mats[:3, 2, :].T,
853
+ overwrite=True)
854
+
855
+ pynibs.coil.write_coil_sequence_xdmf(coil_pos_fn,
856
+ np.arange(n_steps + 1),
857
+ vec1=mats[:3, 0, :].T,
858
+ vec2=mats[:3, 1, :].T,
859
+ vec3=mats[:3, 2, :].T,
860
+ output_xdmf=f"{os.path.splitext(coil_pos_fn)[0]}.xdmf")
1366
861
 
1367
862
  return mats
863
+
864
+
865
+ def shift_coil_to_skin(mesh, coil, matsimnibs, dist=1, dist_neigh=50, coil_elm_idx=None, verbose=False):
866
+ """
867
+ Shifts the coil position to the skin surface, also works for bent coil models.
868
+
869
+ Parameters
870
+ ----------
871
+ mesh : simnibs.mesh_io.Mesh or str
872
+ SimNIBS .msh.
873
+ coil : simnibs.simulation.tms_coil.tms_coil.TmsCoil or str
874
+ .tcd coil model.
875
+ matsimnibs : np.ndarray
876
+ (4, 4) SimNIBS matsimnibs coil position / orientation.
877
+ dist : float, default: 0
878
+ Target (minimum) coil-skin distance.
879
+ dist_neigh : float, default: 50
880
+ If set, only mesh nodes with a distance < dist_neigh are considered for
881
+ the distance calculation to speed up computation.
882
+ coil_elm_idx : list of int, optional
883
+ Which coil elements to use.
884
+ verbose : bool, default: False
885
+ Print output messages.
886
+
887
+ Returns
888
+ -------
889
+ matsimnibs_shifted : np.ndarray
890
+ (4, 4) Shifted coil position / orientation.
891
+ min_dist : float
892
+ (Minimum) coil-skin distance before shifting.
893
+ """
894
+ import trimesh
895
+ import simnibs
896
+ if isinstance(mesh, str):
897
+ mesh = simnibs.mesh_io.read_msh(mesh)
898
+ mesh = mesh.crop_mesh(1005)
899
+
900
+ if isinstance(coil, str):
901
+ from simnibs.simulation.tms_coil.tms_coil import TmsCoil
902
+ coil = TmsCoil.from_file(coil)
903
+
904
+ if verbose:
905
+ pynibs.matsim2paraview(matsimnibs)
906
+
907
+ casing = coil.get_mesh(
908
+ coil_affine=matsimnibs,
909
+ include_optimization_points=False,
910
+ include_coil_elements=False,
911
+ )
912
+ if coil_elm_idx is not None:
913
+ mesh_case = trimesh.Trimesh(vertices=casing.nodes.node_coord,
914
+ faces=casing.elm.node_number_list[coil_elm_idx, :3] - 1)
915
+ casing = casing.nodes.node_coord[casing.elm.node_number_list - 1][coil_elm_idx, :3, :]
916
+ else:
917
+ mesh_case = trimesh.Trimesh(vertices=casing.nodes.node_coord, faces=casing.elm.node_number_list[:, :3] - 1)
918
+ casing = casing.nodes.node_coord[casing.elm.node_number_list - 1][:, :3, :]
919
+
920
+ if dist_neigh is not None:
921
+ close_mesh_nodes = []
922
+ for mesh_node_i, mesh_node in tqdm.tqdm(enumerate(mesh.nodes.node_coord),
923
+ leave=False, total=mesh.nodes.node_coord.shape[0],
924
+ desc=f"Finding close mesh nodes to coil {casing.shape[0]}"):
925
+ if mesh_node_i % 10 != 0:
926
+ # speed this up by subsampling
927
+ continue
928
+ closest = pynibs.util.rotations.points_to_triangles(mesh_node[np.newaxis, :], casing)
929
+
930
+ # if (np.linalg.norm(mesh_node - coil_nodes, axis=1) < dist_neigh).any():
931
+ if (np.linalg.norm(mesh_node - closest[0, ::], axis=1) < dist_neigh).any():
932
+ close_mesh_nodes.append(mesh_node_i + 1)
933
+ if verbose:
934
+ print(f"{len(close_mesh_nodes)} close mesh nodes found.")
935
+
936
+ mesh = mesh.crop_mesh(nodes=close_mesh_nodes)
937
+
938
+ ray_directions = np.repeat(matsimnibs[:3, 2][np.newaxis, :], mesh.nodes.node_coord.shape[0], axis=0)
939
+ loc, index_ray, index_tri = mesh_case.ray.intersects_location(ray_origins=mesh.nodes.node_coord,
940
+ ray_directions=-ray_directions)
941
+
942
+ min_dist = np.linalg.norm(loc - mesh.nodes.node_coord[index_ray], axis=1).min()
943
+ if verbose:
944
+ print(f"Minimum distance to cortex surface: {min_dist} mm")
945
+
946
+ assert min_dist > 0, f"Coil touches head: {min_dist}"
947
+
948
+ # apply the target distance
949
+ min_dist -= dist
950
+
951
+ # shift coil placement in the z-coil axis direction towards the head
952
+ add = min_dist * matsimnibs[:3, 2]
953
+ # print(f"vec: {np.round(matsimnibs[:3, 2], 2)}: by {np.round(add,2)} mm. Mindist: {np.round(min_dist,2)}")
954
+ matsimnibs[:3, 3] += add
955
+
956
+ return matsimnibs, min_dist
957
+
958
+
959
+ def shift_many_coil_placements(mesh, coil, matsimnibs, dist=1, dist_neigh=50, flat_coil=False,
960
+ z_cut_off=None, verbose=False):
961
+ """
962
+ Shifts many coil placements to the cortex surface.
963
+
964
+ Parameters
965
+ ----------
966
+ mesh : simnibs.mesh_io.Mesh or str
967
+ SimNIBD .msh.
968
+ coil : simnibs.simulation.tms_coil.tms_coil.TmsCoil or str
969
+ .tcd coil model.
970
+ matsimnibs : np.ndarray
971
+ (4, 4, N) SimNIBS matsimnibs coil position / orientation.
972
+ dist : float, default: 0
973
+ Target (minimum) coil-skin distance.
974
+ dist_neigh : float, default: 50
975
+ If set, only mesh nodes with a distance < dist_neigh are considered for
976
+ the distance calculation to speed up computation.
977
+ flat_coil : bool, default: False
978
+ If True, the coil is assumed to be flat and shifts for the same position are applied across all rotations.
979
+ z_cut_off : float, optional
980
+ Cut coil case at this z-offset to speed up computation
981
+ verbose : bool, default: False
982
+ Print output messages.
983
+
984
+ Returns
985
+ -------
986
+ matsimnibs : np.ndarray
987
+ (4, 4, N) Shifted coil positions / orientations.
988
+ """
989
+ import simnibs
990
+ if isinstance(mesh, str):
991
+ mesh = simnibs.mesh_io.read_msh(mesh)
992
+ mesh = mesh.crop_mesh(1005)
993
+ if isinstance(coil, str):
994
+ from simnibs.simulation.tms_coil.tms_coil import TmsCoil
995
+ coil = TmsCoil.from_file(coil)
996
+
997
+ if isinstance(matsimnibs, str):
998
+ with h5py.File(matsimnibs, 'r') as f:
999
+ matsimnibs = f['matsimnibs'][:]
1000
+
1001
+ shifts = np.zeros((3, matsimnibs.shape[2]))
1002
+
1003
+ # just look at bottom side of coil to speed up things
1004
+ if z_cut_off is not None:
1005
+ casing = coil.get_mesh(
1006
+ coil_affine=None,
1007
+ include_optimization_points=False,
1008
+ include_coil_elements=False,
1009
+ )
1010
+ casing = casing.nodes.node_coord[casing.elm.node_number_list - 1][:, :3, :]
1011
+ bottom_coil_idx = np.squeeze(np.argwhere(np.sum(casing[:, :, 2] > z_cut_off, axis=1) > 1))
1012
+ assert bottom_coil_idx.shape[0] > 0, "No coil elements found above z_cut_off"
1013
+ else:
1014
+ bottom_coil_idx = None
1015
+
1016
+ # perform shift_coil_to_cortex for each matsimnibs
1017
+ for idx in tqdm.tqdm(range(matsimnibs.shape[2]), position=1, desc="Shift coils"):
1018
+ if (shifts[:, idx] != 0).any():
1019
+ # let's skip the ones we already know
1020
+ continue
1021
+ matsim = matsimnibs[:, :, idx].copy()
1022
+
1023
+ # get shifted coil placement for this idx
1024
+ matsim_shifted, _ = shift_coil_to_skin(mesh, coil, matsim, dist=dist, dist_neigh=dist_neigh,
1025
+ coil_elm_idx=bottom_coil_idx, verbose=verbose)
1026
+
1027
+ if flat_coil:
1028
+ # let's see if there are other coil placements with the same position
1029
+ same_pos_idx = \
1030
+ np.where(np.sum(matsimnibs[:3, 3, :] == matsimnibs[:3, 3, idx][:, np.newaxis], axis=0) == 3)[0]
1031
+ else:
1032
+ same_pos_idx = np.array([idx])
1033
+ shifts[:, same_pos_idx] = matsim_shifted[:3, 3][:, np.newaxis]
1034
+
1035
+ matsimnibs[:3, 3, :] = shifts
1036
+
1037
+ return matsimnibs