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/expio/localite.py CHANGED
@@ -20,6 +20,7 @@ def get_tms_elements(xml_paths, verbose=False):
20
20
  ----------
21
21
  xml_paths : list of str or str
22
22
  Paths to coil0-file and optionally coil1-file. If there is no coil1-file, use empty string.
23
+ Paths to coil0-file and optionally coil1-file. If there is no coil1-file, use empty string.
23
24
  verbose : bool, default: False
24
25
  Flag indicating verbosity.
25
26
 
@@ -99,8 +100,8 @@ def get_tms_elements(xml_paths, verbose=False):
99
100
  xml_rv = coil0_tm.find('ResponseValues')
100
101
  xml_va = xml_rv.findall('Value')
101
102
 
102
- # if valueA is NaN, compute dI/dt with amplitudeA
103
- if xml_va[0].get('response') == 'NaN':
103
+ # if valueA is nan, compute dI/dt with amplitudeA
104
+ if xml_va[0].get('response') == 'nan':
104
105
  current_lst.append(str(round(float(xml_va[2].get('response')) * 1.4461))) # was 1.38
105
106
  else:
106
107
  current_lst.append(xml_va[0].get('response'))
@@ -179,7 +180,7 @@ def match_instrument_marker_string(xml_paths, condition_order):
179
180
  -------
180
181
  coil_cond_lst : list of str
181
182
  Strings containing the right conditions.
182
- drop_idx : list of int
183
+ drop_idx : list of int
183
184
  Indices of trigger markers that were dropped.
184
185
  """
185
186
  drop_idx = []
@@ -568,7 +569,6 @@ def read_triggermarker(fn_xml):
568
569
  m[im_index1, im_index2, 0] = (float(im_ma.get('data' + str(im_index1) + str(im_index2))))
569
570
 
570
571
  # check if coil position is untracked
571
- np.eye(4)
572
572
  if (m[:, :, 0] == np.eye(4)).all():
573
573
  print(f"Untracked coil position found for idx {m_nnav.shape[2]}.")
574
574
  m[:3, :4, 0] = np.nan
@@ -640,7 +640,7 @@ def merge_exp_data_localite(subject, coil_outlier_corr_cond, remove_coil_skin_di
640
640
  im_lst.append(im_lst[0])
641
641
 
642
642
  # handle coil serial numbers
643
- coil_sn_lst = pynibs.get_coil_sn_lst(fn_coil)
643
+ coil_sn_lst = pynibs.expio.get_coil_sn_lst(fn_coil)
644
644
 
645
645
  # get TMS pulse onset
646
646
  tms_pulse_time = subject.exp[exp_idx]['tms_pulse_time']
@@ -678,7 +678,7 @@ def merge_exp_data_localite(subject, coil_outlier_corr_cond, remove_coil_skin_di
678
678
  len_conds.append(len(dict_lst) - len_conds[-1])
679
679
 
680
680
  # convert list of dict to dict of list
681
- results_dct = pynibs.list2dict(dict_lst)
681
+ results_dct = pynibs.util.utils.list2dict(dict_lst)
682
682
 
683
683
  # check if we have a single pulse TMS experiments where every pulse is one condition
684
684
  single_pulse_experiment = np.zeros(len(len_conds))
@@ -744,25 +744,25 @@ def merge_exp_data_localite(subject, coil_outlier_corr_cond, remove_coil_skin_di
744
744
  #######################################################
745
745
  if coil_outlier_corr_cond:
746
746
  print("Removing coil position outliers")
747
- results_dct = pynibs.coil_outlier_correction_cond(exp=results_dct,
748
- outlier_angle=5.,
749
- outlier_loc=3.,
750
- fn_exp_out=fn_exp_hdf5)
747
+ results_dct = pynibs.expio.coil_outlier_correction_cond(exp=results_dct,
748
+ outlier_angle=5.,
749
+ outlier_loc=3.,
750
+ fn_exp_out=fn_exp_hdf5)
751
751
 
752
752
  # perform coil <-> head distance correction
753
753
  ###########################################
754
754
  if coil_distance_corr:
755
755
  print("Performing coil <-> head distance correction")
756
- results_dct = pynibs.coil_distance_correction(exp=results_dct,
757
- fn_geo_hdf5=fn_mesh_hdf5,
758
- remove_coil_skin_distance_outlier=remove_coil_skin_distance_outlier,
759
- fn_plot=os.path.split(fn_exp_hdf5)[0])
756
+ results_dct = pynibs.expio.coil_distance_correction(exp=results_dct,
757
+ fn_geo_hdf5=fn_mesh_hdf5,
758
+ remove_coil_skin_distance_outlier=remove_coil_skin_distance_outlier,
759
+ fn_plot=os.path.split(fn_exp_hdf5)[0])
760
760
 
761
761
  # plot finally used mep data
762
762
  ############################
763
763
  if plot:
764
764
  print("Creating MEP plots ...")
765
- sampling_rate = pynibs.get_mep_sampling_rate(cfs_paths[0])
765
+ sampling_rate = pynibs.expio.get_mep_sampling_rate(cfs_paths[0])
766
766
 
767
767
  # Compute start and stop idx according to sampling rate
768
768
  start_mep = int((20 / 1000.) * sampling_rate)
@@ -848,12 +848,9 @@ def merge_exp_data_localite(subject, coil_outlier_corr_cond, remove_coil_skin_di
848
848
  df_phys_data_postproc_emg = pd.DataFrame.from_dict(phys_data_postproc_emg)
849
849
 
850
850
  # save in .hdf5 file
851
- df_stim_data.to_hdf(fn_exp_hdf5, "stim_data")
852
- # df_stim_data[['coil_mean']].to_hdf(fn_exp_hdf5, "stim_data")
853
- # [print(type(df_stim_data[['coil_mean']].iloc[0].values)) for i in range(df_stim_data.shape[0])]
854
- # df_stim_data.columns
855
- df_phys_data_postproc_emg.to_hdf(fn_exp_hdf5, "phys_data/postproc/EMG")
856
- df_phys_data_raw_emg.to_hdf(fn_exp_hdf5, "phys_data/raw/EMG")
851
+ df_stim_data.to_hdf(fn_exp_hdf5, key="stim_data")
852
+ df_phys_data_postproc_emg.to_hdf(fn_exp_hdf5, key="phys_data/postproc/EMG")
853
+ df_phys_data_raw_emg.to_hdf(fn_exp_hdf5, key="phys_data/raw/EMG")
857
854
 
858
855
  with h5py.File(fn_exp_hdf5, "a") as f:
859
856
  f.create_dataset(name="stim_data/info/channels", data=np.array(channels).astype("|S"))
@@ -909,7 +906,7 @@ def merge_exp_data_rt(subject, coil_outlier_corr_cond, remove_coil_skin_distance
909
906
  im_lst.append(im_lst[0])
910
907
 
911
908
  # handle coil serial numbers
912
- coil_sn_lst = pynibs.get_coil_sn_lst(fn_coil)
909
+ coil_sn_lst = pynibs.expio.get_coil_sn_lst(fn_coil)
913
910
 
914
911
  # get TMS pulse onset
915
912
  tms_pulse_time = subject.exp[exp_idx]['tms_pulse_time']
@@ -938,7 +935,7 @@ def merge_exp_data_rt(subject, coil_outlier_corr_cond, remove_coil_skin_distance
938
935
  len_conds.append(len(dict_lst) - len_conds[-1])
939
936
 
940
937
  # convert list of dict to dict of list
941
- results_dct = pynibs.list2dict(dict_lst)
938
+ results_dct = pynibs.util.utils.list2dict(dict_lst)
942
939
 
943
940
  # check if we have a single pulse TMS experiments where every pulse is one condition
944
941
  single_pulse_experiment = np.zeros(len(len_conds))
@@ -992,19 +989,19 @@ def merge_exp_data_rt(subject, coil_outlier_corr_cond, remove_coil_skin_distance
992
989
  #######################################################
993
990
  if coil_outlier_corr_cond:
994
991
  print("Removing coil position outliers")
995
- results_dct = pynibs.coil_outlier_correction_cond(exp=results_dct,
996
- outlier_angle=5.,
997
- outlier_loc=3.,
998
- fn_exp_out=fn_exp_hdf5)
992
+ results_dct = pynibs.expio.coil_outlier_correction_cond(exp=results_dct,
993
+ outlier_angle=5.,
994
+ outlier_loc=3.,
995
+ fn_exp_out=fn_exp_hdf5)
999
996
 
1000
997
  # perform coil <-> head distance correction
1001
998
  ###########################################
1002
999
  if coil_distance_corr:
1003
1000
  print("Performing coil <-> head distance correction")
1004
- results_dct = pynibs.coil_distance_correction(exp=results_dct,
1005
- fn_geo_hdf5=fn_mesh_hdf5,
1006
- remove_coil_skin_distance_outlier=remove_coil_skin_distance_outlier,
1007
- fn_plot=os.path.split(fn_exp_hdf5)[0])
1001
+ results_dct = pynibs.expio.coil_distance_correction(exp=results_dct,
1002
+ fn_geo_hdf5=fn_mesh_hdf5,
1003
+ remove_coil_skin_distance_outlier=remove_coil_skin_distance_outlier,
1004
+ fn_plot=os.path.split(fn_exp_hdf5)[0])
1008
1005
 
1009
1006
  # plot finally used rt data
1010
1007
  ############################
@@ -1018,7 +1015,7 @@ def merge_exp_data_rt(subject, coil_outlier_corr_cond, remove_coil_skin_distance
1018
1015
  average_y = []
1019
1016
  for ind in range(len(y) - avg_window + 1):
1020
1017
  average_y.append(np.mean(y[ind:ind + avg_window]))
1021
- # insert NaNs to match lengths
1018
+ # insert nan to match lengths
1022
1019
  for ind in range(avg_window - 1):
1023
1020
  average_y.insert(0, np.nan)
1024
1021
 
@@ -1058,8 +1055,8 @@ def merge_exp_data_rt(subject, coil_outlier_corr_cond, remove_coil_skin_distance
1058
1055
  df_behave_data = pd.DataFrame.from_dict(behave_dict)
1059
1056
 
1060
1057
  # save in .hdf5 file
1061
- df_stim_data.to_hdf(fn_exp_hdf5, "stim_data")
1062
- df_behave_data.to_hdf(fn_exp_hdf5, "behavioral_data")
1058
+ df_stim_data.to_hdf(fn_exp_hdf5, key="stim_data")
1059
+ df_behave_data.to_hdf(fn_exp_hdf5, key="behavioral_data")
1063
1060
 
1064
1061
 
1065
1062
  def merge_exp_data_ft(subject, coil_outlier_corr_cond, remove_coil_skin_distance_outlier, coil_distance_corr,
@@ -1112,7 +1109,7 @@ def merge_exp_data_ft(subject, coil_outlier_corr_cond, remove_coil_skin_distance
1112
1109
  im_lst.append(im_lst[0])
1113
1110
 
1114
1111
  # handle coil serial numbers
1115
- coil_sn_lst = pynibs.get_coil_sn_lst(fn_coil)
1112
+ coil_sn_lst = pynibs.util.utlis.get_coil_sn_lst(fn_coil)
1116
1113
 
1117
1114
  # get TMS pulse onset
1118
1115
  tms_pulse_time = subject.exp[exp_idx]['tms_pulse_time']
@@ -1141,7 +1138,7 @@ def merge_exp_data_ft(subject, coil_outlier_corr_cond, remove_coil_skin_distance
1141
1138
  len_conds.append(len(dict_lst) - len_conds[-1])
1142
1139
 
1143
1140
  # convert list of dict to dict of list
1144
- results_dct = pynibs.list2dict(dict_lst)
1141
+ results_dct = pynibs.util.utils.list2dict(dict_lst)
1145
1142
 
1146
1143
  # check if we have a single pulse TMS experiments where every pulse is one condition
1147
1144
  single_pulse_experiment = np.zeros(len(len_conds))
@@ -1201,19 +1198,19 @@ def merge_exp_data_ft(subject, coil_outlier_corr_cond, remove_coil_skin_distance
1201
1198
  #######################################################
1202
1199
  if coil_outlier_corr_cond:
1203
1200
  print("Removing coil position outliers")
1204
- results_dct = pynibs.coil_outlier_correction_cond(exp=results_dct,
1205
- outlier_angle=5.,
1206
- outlier_loc=3.,
1207
- fn_exp_out=fn_exp_hdf5)
1201
+ results_dct = pynibs.expio.coil_outlier_correction_cond(exp=results_dct,
1202
+ outlier_angle=5.,
1203
+ outlier_loc=3.,
1204
+ fn_exp_out=fn_exp_hdf5)
1208
1205
 
1209
1206
  # perform coil <-> head distance correction
1210
1207
  ###########################################
1211
1208
  if coil_distance_corr:
1212
1209
  print("Performing coil <-> head distance correction")
1213
- results_dct = pynibs.coil_distance_correction(exp=results_dct,
1214
- fn_geo_hdf5=fn_mesh_hdf5,
1215
- remove_coil_skin_distance_outlier=remove_coil_skin_distance_outlier,
1216
- fn_plot=os.path.split(fn_exp_hdf5)[0])
1210
+ results_dct = pynibs.expio.coil_distance_correction(exp=results_dct,
1211
+ fn_geo_hdf5=fn_mesh_hdf5,
1212
+ remove_coil_skin_distance_outlier=remove_coil_skin_distance_outlier,
1213
+ fn_plot=os.path.split(fn_exp_hdf5)[0])
1217
1214
 
1218
1215
  # plot finally used ft data
1219
1216
  ############################
@@ -1227,7 +1224,7 @@ def merge_exp_data_ft(subject, coil_outlier_corr_cond, remove_coil_skin_distance
1227
1224
  average_y = []
1228
1225
  for ind in range(len(y) - avg_window + 1):
1229
1226
  average_y.append(np.mean(y[ind:ind + avg_window]))
1230
- # insert NaNs to match lengths
1227
+ # insert nan to match lengths
1231
1228
  for ind in range(avg_window - 1):
1232
1229
  average_y.insert(0, np.nan)
1233
1230
 
@@ -1267,8 +1264,8 @@ def merge_exp_data_ft(subject, coil_outlier_corr_cond, remove_coil_skin_distance
1267
1264
  df_behave_data = pd.DataFrame.from_dict(behave_dict)
1268
1265
 
1269
1266
  # save in .hdf5 file
1270
- df_stim_data.to_hdf(fn_exp_hdf5, "stim_data")
1271
- df_behave_data.to_hdf(fn_exp_hdf5, "behavioral_data")
1267
+ df_stim_data.to_hdf(fn_exp_hdf5, key="stim_data")
1268
+ df_behave_data.to_hdf(fn_exp_hdf5, key="behavioral_data")
1272
1269
 
1273
1270
 
1274
1271
  def match_behave_and_triggermarker(mep_time_lst, xml_paths, bnd_factor=0.99 / 2, isi=None):
@@ -1301,7 +1298,7 @@ def match_behave_and_triggermarker(mep_time_lst, xml_paths, bnd_factor=0.99 / 2,
1301
1298
  # _, mep_time_lst_tmp = get_mep_elements(cfs_path, tms_pulse_time)
1302
1299
  # mep_time_lst.extend(mep_time_lst_tmp)
1303
1300
 
1304
- _, tms_ts_lst, _, tms_idx_invalid = pynibs.localite.get_tms_elements(xml_paths, verbose=True)
1301
+ _, tms_ts_lst, _, tms_idx_invalid = get_tms_elements(xml_paths, verbose=True)
1305
1302
 
1306
1303
  # get timestamp difference of mep measurements
1307
1304
  if isi is None:
@@ -1474,7 +1471,7 @@ def combine_nnav_mep(xml_paths, cfs_paths, im, coil_sn,
1474
1471
  'patient_id'
1475
1472
  """
1476
1473
  # get arrays and lists
1477
- coil_array, ts_tms_lst, current_lst, tms_idx_invalid = pynibs.get_tms_elements(xml_paths, verbose=False)
1474
+ coil_array, ts_tms_lst, current_lst, tms_idx_invalid = get_tms_elements(xml_paths, verbose=False)
1478
1475
 
1479
1476
  # get MEP amplitudes from .cfs files
1480
1477
  time_mep_lst, mep_latencies = [], []
@@ -1489,14 +1486,14 @@ def combine_nnav_mep(xml_paths, cfs_paths, im, coil_sn,
1489
1486
  # calc MEP amplitudes and MEP onset times from .cfs file
1490
1487
  p2p_array_tmp, time_mep_lst_tmp, \
1491
1488
  mep_raw_data_tmp, mep_filt_data_tmp, \
1492
- mep_raw_data_time, mep_latency = pynibs.get_mep_elements(mep_fn=cfs_path,
1493
- tms_pulse_time=tms_pulse_time,
1494
- drop_mep_idx=drop_mep_idx,
1495
- cfs_data_column=cfs_data_column,
1496
- channels=channels,
1497
- plot=plot,
1498
- start_mep=start_mep,
1499
- end_mep=end_mep)
1489
+ mep_raw_data_time, mep_latency = pynibs.expio.signal_ced.get_mep_elements(mep_fn=cfs_path,
1490
+ tms_pulse_time=tms_pulse_time,
1491
+ drop_mep_idx=drop_mep_idx,
1492
+ cfs_data_column=cfs_data_column,
1493
+ channels=channels,
1494
+ plot=plot,
1495
+ start_mep=start_mep,
1496
+ end_mep=end_mep)
1500
1497
 
1501
1498
  # add .cfs onsets from subject object and add onset of last mep from last .cfs file
1502
1499
  if mep_onsets is not None:
@@ -1525,7 +1522,7 @@ def combine_nnav_mep(xml_paths, cfs_paths, im, coil_sn,
1525
1522
  bnd_factor=0.99 / 2) # 0.99/2
1526
1523
 
1527
1524
  if cfs_paths[0].endswith("cfs"):
1528
- experiment_date_time = pynibs.get_time_date(cfs_paths)
1525
+ experiment_date_time = pynibs.expio.get_time_date(cfs_paths)
1529
1526
  else:
1530
1527
  experiment_date_time = "N/A"
1531
1528
 
@@ -1543,20 +1540,20 @@ def combine_nnav_mep(xml_paths, cfs_paths, im, coil_sn,
1543
1540
  else:
1544
1541
  # get conditions from instrument markers
1545
1542
  if os.path.isfile(im[0]):
1546
- coil_cond_lst, drop_idx = pynibs.match_instrument_marker_file(xml_paths, im[0])
1543
+ coil_cond_lst, drop_idx = match_instrument_marker_file(xml_paths, im[0])
1547
1544
  else:
1548
- coil_cond_lst, drop_idx = pynibs.match_instrument_marker_string(xml_paths, im)
1545
+ coil_cond_lst, drop_idx = match_instrument_marker_string(xml_paths, im)
1549
1546
 
1550
1547
  # coordinate transform (for coil_0, coil_1, coil_mean)
1551
1548
  for idx in range(coil_array.shape[0]):
1552
1549
  # move axis, calculate and move back
1553
1550
  m_simnibs = np.moveaxis(coil_array[idx, :, :, :], 0, 2)
1554
- m_simnibs = pynibs.nnav2simnibs(fn_exp_nii=nii_exp_path[0],
1555
- fn_conform_nii=nii_conform_path,
1556
- m_nnav=m_simnibs,
1557
- nnav_system=nnav_system,
1558
- mesh_approach=mesh_approach,
1559
- temp_dir=temp_dir)
1551
+ m_simnibs = pynibs.expio.nnav2simnibs(fn_exp_nii=nii_exp_path[0],
1552
+ fn_conform_nii=nii_conform_path,
1553
+ m_nnav=m_simnibs,
1554
+ nnav_system=nnav_system,
1555
+ mesh_approach=mesh_approach,
1556
+ temp_dir=temp_dir)
1560
1557
 
1561
1558
  coil_array[idx, :, :, :] = np.moveaxis(m_simnibs, 2, 0)
1562
1559
 
@@ -1680,7 +1677,7 @@ def combine_nnav_rt(xml_paths, behavior_paths, im, coil_sn,
1680
1677
  """
1681
1678
 
1682
1679
  # get arrays and lists
1683
- coil_array, ts_tms_lst, current_lst, tms_idx_invalid = pynibs.get_tms_elements(xml_paths, verbose=False)
1680
+ coil_array, ts_tms_lst, current_lst, tms_idx_invalid = get_tms_elements(xml_paths, verbose=False)
1684
1681
 
1685
1682
  # get RT from .csv files
1686
1683
  time_mep_lst = []
@@ -1692,10 +1689,10 @@ def combine_nnav_rt(xml_paths, behavior_paths, im, coil_sn,
1692
1689
 
1693
1690
  for idx, behavior_path in enumerate(behavior_paths):
1694
1691
  # get RT and trial onsets from .csv file
1695
- rt_array_tmp, trial_onset_lst_tmp, mean_isi = pynibs.get_trial_data_from_csv(behavior_fn=behavior_path,
1696
- drop_trial_idx=drop_trial_idx,
1697
- cond=cond,
1698
- only_corr=True)
1692
+ rt_array_tmp, trial_onset_lst_tmp, mean_isi = pynibs.expio.get_trial_data_from_csv(behavior_fn=behavior_path,
1693
+ drop_trial_idx=drop_trial_idx,
1694
+ cond=cond,
1695
+ only_corr=True)
1699
1696
 
1700
1697
  time_mep_lst.extend(trial_onset_lst_tmp)
1701
1698
 
@@ -1729,20 +1726,20 @@ def combine_nnav_rt(xml_paths, behavior_paths, im, coil_sn,
1729
1726
  else:
1730
1727
  # get conditions from instrument markers
1731
1728
  if os.path.isfile(im[0]):
1732
- coil_cond_lst, drop_idx = pynibs.match_instrument_marker_file(xml_paths, im[0])
1729
+ coil_cond_lst, drop_idx = match_instrument_marker_file(xml_paths, im[0])
1733
1730
  else:
1734
- coil_cond_lst, drop_idx = pynibs.match_instrument_marker_string(xml_paths, im)
1731
+ coil_cond_lst, drop_idx = match_instrument_marker_string(xml_paths, im)
1735
1732
 
1736
1733
  # coordinate transform (for coil_0, coil_1, coil_mean)
1737
1734
  for idx in range(coil_array.shape[0]):
1738
1735
  # move axis, calculate and move back
1739
1736
  m_simnibs = np.moveaxis(coil_array[idx, :, :, :], 0, 2)
1740
- m_simnibs = pynibs.nnav2simnibs(fn_exp_nii=nii_exp_path[0],
1741
- fn_conform_nii=nii_conform_path,
1742
- m_nnav=m_simnibs,
1743
- nnav_system=nnav_system,
1744
- mesh_approach=mesh_approach,
1745
- temp_dir=temp_dir)
1737
+ m_simnibs = pynibs.expio.nnav2simnibs(fn_exp_nii=nii_exp_path[0],
1738
+ fn_conform_nii=nii_conform_path,
1739
+ m_nnav=m_simnibs,
1740
+ nnav_system=nnav_system,
1741
+ mesh_approach=mesh_approach,
1742
+ temp_dir=temp_dir)
1746
1743
 
1747
1744
  coil_array[idx, :, :, :] = np.moveaxis(m_simnibs, 2, 0)
1748
1745
 
@@ -1862,7 +1859,7 @@ def combine_nnav_ft(xml_paths, behavior_paths, im, coil_sn,
1862
1859
  """
1863
1860
 
1864
1861
  # get arrays and lists
1865
- coil_array, ts_tms_lst, current_lst, tms_idx = pynibs.get_tms_elements(xml_paths, verbose=False)
1862
+ coil_array, ts_tms_lst, current_lst, tms_idx = get_tms_elements(xml_paths, verbose=False)
1866
1863
 
1867
1864
  # get finger tapping data from .csv files
1868
1865
  time_ft_lst = []
@@ -1874,9 +1871,9 @@ def combine_nnav_ft(xml_paths, behavior_paths, im, coil_sn,
1874
1871
 
1875
1872
  for idx, behavior_path in enumerate(behavior_paths):
1876
1873
  # get finger tapping data and trial onsets from .csv file
1877
- ft_array_tmp, trial_onset_lst_tmp, mean_isi = pynibs.get_ft_data_from_csv(behavior_fn=behavior_path,
1878
- drop_trial_idx=drop_trial_idx,
1879
- cond=cond)
1874
+ ft_array_tmp, trial_onset_lst_tmp, mean_isi = get_ft_data_from_csv(behavior_fn=behavior_path,
1875
+ drop_trial_idx=drop_trial_idx,
1876
+ cond=cond)
1880
1877
 
1881
1878
  time_ft_lst.extend(trial_onset_lst_tmp)
1882
1879
 
@@ -1910,20 +1907,20 @@ def combine_nnav_ft(xml_paths, behavior_paths, im, coil_sn,
1910
1907
  else:
1911
1908
  # get conditions from instrument markers
1912
1909
  if os.path.isfile(im[0]):
1913
- coil_cond_lst, drop_idx = pynibs.match_instrument_marker_file(xml_paths, im[0])
1910
+ coil_cond_lst, drop_idx = match_instrument_marker_file(xml_paths, im[0])
1914
1911
  else:
1915
- coil_cond_lst, drop_idx = pynibs.match_instrument_marker_string(xml_paths, im)
1912
+ coil_cond_lst, drop_idx = match_instrument_marker_string(xml_paths, im)
1916
1913
 
1917
1914
  # coordinate transform (for coil_0, coil_1, coil_mean)
1918
1915
  for idx in range(coil_array.shape[0]):
1919
1916
  # move axis, calculate and move back
1920
1917
  m_simnibs = np.moveaxis(coil_array[idx, :, :, :], 0, 2)
1921
- m_simnibs = pynibs.nnav2simnibs(fn_exp_nii=nii_exp_path[0],
1922
- fn_conform_nii=nii_conform_path,
1923
- m_nnav=m_simnibs,
1924
- nnav_system=nnav_system,
1925
- mesh_approach=mesh_approach,
1926
- temp_dir=temp_dir)
1918
+ m_simnibs = pynibs.expio.nnav2simnibs(fn_exp_nii=nii_exp_path[0],
1919
+ fn_conform_nii=nii_conform_path,
1920
+ m_nnav=m_simnibs,
1921
+ nnav_system=nnav_system,
1922
+ mesh_approach=mesh_approach,
1923
+ temp_dir=temp_dir)
1927
1924
 
1928
1925
  coil_array[idx, :, :, :] = np.moveaxis(m_simnibs, 2, 0)
1929
1926
 
@@ -1985,3 +1982,105 @@ def combine_nnav_ft(xml_paths, behavior_paths, im, coil_sn,
1985
1982
  idx += 1
1986
1983
 
1987
1984
  return dict_lst
1985
+
1986
+
1987
+ def hdf5_from_xml(fn_xml):
1988
+ """
1989
+ Convert a Localite XML TriggerMarker file to HDF5 and XDMF format.
1990
+
1991
+ Parameters
1992
+ ----------
1993
+ fn_xml : str
1994
+ Path to the XML file.
1995
+
1996
+ Returns
1997
+ -------
1998
+ <file> .xdmf/.hdf5 tuple
1999
+ """
2000
+ assert fn_xml.endswith('.xml'), "File must be an XML file"
2001
+ fn_hdf5 = fn_xml.replace('.xml', '.hdf5')
2002
+ fn_xdmf = fn_xml.replace('.xml', '.xdmf')
2003
+
2004
+ for markertype in ['TriggerMarker', 'InstrumentMarker']:
2005
+ m_nnav = get_marker(fn_xml, markertype=markertype)[0]
2006
+ if len(m_nnav):
2007
+ break
2008
+ assert len(m_nnav), f"Could not find any markers in {fn_xml}"
2009
+ # m_nnav, didt, stim_int, descr, rec_time = read_triggermarker(fn_xml)
2010
+ centers = m_nnav[:,:3, 3]
2011
+ n_coils = centers.shape[0]
2012
+
2013
+ m0 = np.squeeze(m_nnav[:, :3, 0]).T
2014
+ m1 = np.squeeze(m_nnav[:, :3, 1]).T
2015
+ m2 = np.squeeze(m_nnav[:, :3, 2]).T
2016
+
2017
+ with h5py.File(fn_hdf5, 'w') as f_hdf5:
2018
+ f_hdf5.create_dataset('centers', data=centers.astype(np.float64))
2019
+ f_hdf5.create_dataset('m0', data=m0.astype(np.float64))
2020
+ f_hdf5.create_dataset('m1', data=m1.astype(np.float64))
2021
+ f_hdf5.create_dataset('m2', data=m2.astype(np.float64))
2022
+ fn_hdf5 = os.path.split(fn_hdf5)[1]
2023
+ with open(fn_xdmf, 'w') as f_xdmf:
2024
+ f_xdmf.write('<?xml version="1.0"?>\n')
2025
+ f_xdmf.write('<!DOCTYPE Xdmf SYSTEM "Xdmf.dtd" []>\n')
2026
+ f_xdmf.write('<Xdmf Version="2.0" xmlns:xi="http://www.w3.org/2001/XInclude">\n')
2027
+ f_xdmf.write('<Domain>\n')
2028
+ f_xdmf.write('<Grid\nCollectionType="Spatial"\nGridType="Collection"\nName="Collection">\n')
2029
+
2030
+ # one grid for coil dipole nodes...store data hdf5.
2031
+ #######################################################
2032
+ f_xdmf.write('<Grid Name="stimsites" GridType="Uniform">\n')
2033
+ f_xdmf.write(f'<Topology NumberOfElements="{n_coils}" TopologyType="Polyvertex" Name="Tri">\n')
2034
+ f_xdmf.write(f'<DataItem Format="XML" Dimensions="{n_coils} 1">\n')
2035
+ # f.write(hdf5_fn + ':' + path + '/triangle_number_list\n')
2036
+ np.savetxt(f_xdmf, list(range(centers.shape[0])), fmt='%d', delimiter=' ') # 1 2 3 4 ... N_Points
2037
+ f_xdmf.write('</DataItem>\n')
2038
+ f_xdmf.write('</Topology>\n')
2039
+
2040
+ # nodes
2041
+ f_xdmf.write('<Geometry GeometryType="XYZ">\n')
2042
+ f_xdmf.write(f'<DataItem Format="HDF" Dimensions="{n_coils} 3">\n')
2043
+ f_xdmf.write(f'{fn_hdf5}:/centers\n')
2044
+ f_xdmf.write('</DataItem>\n')
2045
+ f_xdmf.write('</Geometry>\n')
2046
+
2047
+ # data
2048
+ # dipole magnitude
2049
+ # the 4 vectors
2050
+ for i in range(3):
2051
+ f_xdmf.write(f'<Attribute Name="dir_{i}" AttributeType="Vector" Center="Cell">\n')
2052
+ f_xdmf.write(f'<DataItem Format="HDF" Dimensions="{n_coils} 3">\n')
2053
+ f_xdmf.write(f"{fn_hdf5}:/m{i}\n")
2054
+ f_xdmf.write('</DataItem>\n')
2055
+ f_xdmf.write('</Attribute>\n\n')
2056
+
2057
+ # angles
2058
+ # f_xdmf.write('<Attribute Name="angles" AttributeType="Scalar" Center="Cell">\n')
2059
+ # f_xdmf.write(f'<DataItem Format="HDF" Dimensions="{centers.shape[0]} 1">\n')
2060
+ # f_xdmf.write(f'{fn_hdf5}:/angles\n')
2061
+ # f_xdmf.write('</DataItem>\n')
2062
+ # f_xdmf.write('</Attribute>\n\n')
2063
+
2064
+ # data
2065
+ # if data_dict is not None:
2066
+ # f_xdmf.write('<Attribute Name="data" AttributeType="Scalar" Center="Cell">\n')
2067
+ # f_xdmf.write('<DataItem Format="HDF" Dimensions="' + str(data.shape[0]) + ' 1">\n')
2068
+ # f_xdmf.write(f'{fn_hdf5}:/data\n')
2069
+ # f_xdmf.write('</DataItem>\n')
2070
+ # f_xdmf.write('</Attribute>\n\n')
2071
+
2072
+ # site idx
2073
+ # f_xdmf.write('<Attribute Name="sites_idx" AttributeType="Scalar" Center="Cell">\n')
2074
+ # f_xdmf.write(f'<DataItem Format="HDF" Dimensions="{centers.shape[0]} 1">\n')
2075
+ # f_xdmf.write(f'{fn_hdf5}:/sites_idx\n')
2076
+ # f_xdmf.write('</DataItem>\n')
2077
+ # f_xdmf.write('</Attribute>\n\n')
2078
+
2079
+ f_xdmf.write('</Grid>\n')
2080
+ # end coil dipole data
2081
+
2082
+ # footer
2083
+ f_xdmf.write('</Grid>\n')
2084
+ f_xdmf.write('</Domain>\n')
2085
+ f_xdmf.write('</Xdmf>\n')
2086
+ f_xdmf.close()
@@ -0,0 +1,139 @@
1
+ import glob
2
+ import os
3
+ import pynibs
4
+ import datetime
5
+ import numpy as np
6
+
7
+
8
+ def get_mep_elements(session_path, chan_idx=None, recording_id=None,
9
+ tms_port_id=2, mep_start_ms=16, mep_end_ms=40, plot_folder=None, verbose=False):
10
+ """
11
+ This function calculates the MEP elements for a given session and channel for Neurone Tesla EEG data.
12
+
13
+ Parameters
14
+ ----------
15
+ session_path : str
16
+ Path to the session folder.
17
+ mep_start_ms : int
18
+ Start time of the MEP in ms.
19
+ mep_end_ms : int
20
+ End time of the MEP in ms.
21
+ chan_idx : int, optional
22
+ Channel index. Default: All channels.
23
+ recording_id : int, optional
24
+ Recording ID. Default: All recordings
25
+ tms_port_id : int
26
+ TMS port ID for the pulse trigger.
27
+ verbose : bool
28
+ Print verbose output.
29
+ plot_folder : str, optional
30
+ Folder to store the plots.
31
+
32
+ Returns
33
+ -------
34
+ p2p_array : np.ndarray of float
35
+ (N_stim) Peak to peak values of N sweeps.
36
+ time_mep_lst : list of datetime.timedelta
37
+ MEP-timestamps
38
+ mep_raw_data : np.ndarray of float
39
+ (N_channel, N_stim, N_samples) Raw (unfiltered) MEP data.
40
+ mep_filt_data : np.ndarray of float
41
+ (N_channel, N_stim, N_samples) Filtered MEP data (Butterworth lowpass filter).
42
+ time : np.ndarray of float
43
+ (N_samples) Time axis corresponding to MEP data.
44
+ mep_onset_array : np.ndarray of float
45
+ (S_samples) MEP onset after TMS pulse.
46
+ """
47
+ from neurone_tools import neurone_tools
48
+ # read all recordings in the session
49
+ if recording_id is None:
50
+ subdirs = glob.glob(f"{session_path}/*")
51
+ n_subdirs = len([os.path.split(subdir)[1] for subdir in subdirs if os.path.isdir(subdir)])
52
+ if verbose:
53
+ print(f"Found {n_subdirs} recordings in session {session_path}")
54
+ recording_ids = list(range(1, n_subdirs+1))
55
+ else:
56
+ recording_ids = [recording_id]
57
+
58
+ # read all channels in the recording
59
+ nt = neurone_tools(session_path, recording_ids[0], channels='all')
60
+ if chan_idx is None:
61
+ chan_ids = range(len(nt.get_channel_info()))
62
+ if verbose:
63
+ print(f"Found {len(chan_ids)} channels in recording {recording_id}")
64
+ else:
65
+ chan_ids = [chan_idx]
66
+
67
+ p2p_arr = []
68
+ mep_raw_arr = []
69
+ mep_filt_arr = []
70
+ mep_onset_arr = []
71
+ time_tms_pulse_list_chan = []
72
+ time_axes_chan = []
73
+
74
+ for chan_idx in chan_ids:
75
+ p2p_arr_chan = []
76
+ time_tms_pulse_list_chan = []
77
+ mep_raw_arr_chan = []
78
+ mep_filt_arr_chan = []
79
+ time_axes_chan = []
80
+ mep_onset_arr_chan = []
81
+
82
+ for recording_id in recording_ids:
83
+ nt = neurone_tools(session_path, recording_id, channels='all')
84
+ fsample = nt.get_sampling_rate()
85
+
86
+ if nt.get_channel_info()[chan_idx]['Unit'] == 'µV':
87
+ fac = 1/1000
88
+ else:
89
+ raise ValueError(f"Unknown unit {nt.get_channel_info()[chan_idx]['Unit']}")
90
+
91
+ # mep_start_sample = int(fsample * mep_start_ms/1000)
92
+ mep_end_sample = int(fsample * mep_end_ms*2/1000)
93
+
94
+ events = nt.load_events()
95
+ tms_pulse_idx = events['start_sample_index'][events['source_port'] == tms_port_id].values
96
+ data, chan_names = nt.load_data()
97
+ if not len(tms_pulse_idx):
98
+ print(f"No TMS pulses found in session {recording_id}")
99
+ continue
100
+ timedelta = datetime.timedelta(milliseconds=0)
101
+ for pulse_id, pulse in enumerate(tms_pulse_idx):
102
+ sweep = data[pulse:int(mep_end_sample + pulse), chan_idx]
103
+ sweep *= fac
104
+ sweep -= sweep.mean()
105
+
106
+ if plot_folder is not None:
107
+ fn_plot = f"{plot_folder}/ses{recording_id:0>2}_chan{chan_idx}_{pulse_id:0>3}_mep.png"
108
+ else:
109
+ fn_plot = None
110
+ p2p, sweep_filt, onset = pynibs.expio.calc_p2p(sweep, tms_pulse_time=0,
111
+ start_mep=mep_start_ms,
112
+ end_mep=mep_end_ms,
113
+ measurement_start_time=0,
114
+ sampling_rate=fsample,
115
+ cutoff_freq=500,
116
+ fn_plot=fn_plot)
117
+ p2p_arr_chan.append(p2p)
118
+
119
+ time_tms_pulse_list_chan.append(timedelta)
120
+ timedelta += datetime.timedelta(milliseconds=1000 * (pulse / fsample))
121
+ mep_filt_arr_chan.append(sweep_filt)
122
+ mep_onset_arr_chan.append(onset)
123
+ mep_raw_arr_chan.append(sweep)
124
+ time_axes_chan.append(np.arange(sweep.shape[0]) / fsample)
125
+
126
+ print(f"channel {chan_idx} session {recording_id: >2}: {(events['source_port'] == tms_port_id).sum()}")
127
+ mep_raw_arr_chan = np.vstack(mep_raw_arr_chan)
128
+ mep_filt_arr_chan = np.vstack(mep_filt_arr_chan)
129
+
130
+ p2p_arr.append(p2p_arr_chan)
131
+ mep_filt_arr.append(mep_filt_arr_chan)
132
+ mep_onset_arr.append(mep_onset_arr_chan)
133
+ mep_raw_arr.append(mep_raw_arr_chan)
134
+ p2p_arr = np.vstack(p2p_arr)
135
+ mep_raw_arr = np.stack(mep_raw_arr, 0)
136
+ mep_filt_arr = np.stack(mep_filt_arr, 0)
137
+ mep_onset_arr = np.vstack(mep_onset_arr)
138
+
139
+ return p2p_arr, time_tms_pulse_list_chan, mep_raw_arr, mep_filt_arr, time_axes_chan, mep_onset_arr