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/Mep.py CHANGED
@@ -1,17 +1,13 @@
1
1
  import re
2
- import json
3
2
  import pylab
4
3
  import warnings
5
- import datetime
6
4
  import numpy as np
7
- import pandas as pd
8
5
  from lmfit import Model
9
6
  import scipy.io as spio
10
- from scipy.signal import butter, lfilter
11
7
  import matplotlib.pyplot as plt
8
+ from scipy.signal import butter, lfilter
12
9
 
13
10
  import pynibs
14
-
15
11
  from pynibs.expio.fit_funs import sigmoid, sigmoid4_log, linear, exp, exp0
16
12
 
17
13
  try:
@@ -22,11 +18,7 @@ except ImportError:
22
18
  np.seterr(over="ignore")
23
19
 
24
20
 
25
- def get_mep_virtual_subject_TVS(x, p1=-5.0818, p2=-2.4677, p3=3.6466, p4=0.42639, p5=1.6665,
26
- mu_y_add=10 ** -5.0818, mu_y_mult=-0.9645334, mu_x_add=0.68827324,
27
- sigma_y_add=1.4739 * 1e-6, k=0.39316, sigma2_y_mult=2.2759 * 1e-2,
28
- sigma2_x_add=2.3671 * 1e-2,
29
- subject_variability=False, trial_variability=True):
21
+ def get_mep_virtual_subject_TVS(**kwargs):
30
22
  print("get_mep_virtual_subject_TVS() got renamed to get_virtual_mep_tvs()")
31
23
  raise NotImplementedError
32
24
 
@@ -97,11 +89,13 @@ def get_virtual_mep_tvs(x, p1=-5.0818, p2=-2.4677, p3=3.6466, p4=0.42639, p5=1.6
97
89
  e_x_add = 10 ** np.random.normal(loc=mu_x_add, scale=np.sqrt(sigma2_x_add), size=n_x)
98
90
 
99
91
  # determine generalized value distribution of additive y variability
100
- p_e_y_add = pynibs.generalized_extreme_value_distribution(x=np.linspace(5e-6, 1e-4, 100000),
101
- mu=mu_y_add, sigma=sigma_y_add, k=k)
92
+ p_e_y_add = pynibs.util.utils.generalized_extreme_value_distribution(x=np.linspace(5e-6, 1e-4,
93
+ 100000),
94
+ mu=mu_y_add, sigma=sigma_y_add, k=k)
102
95
 
103
96
  # sample from generalized value distribution to determine additive y variability
104
- e_y_add = np.random.choice(np.linspace(5e-6, 1e-4, 100000), p=p_e_y_add / np.sum(p_e_y_add), size=n_x)
97
+ e_y_add = np.random.choice(np.linspace(5e-6, 1e-4, 100000), p=p_e_y_add / np.sum(p_e_y_add),
98
+ size=n_x)
105
99
 
106
100
  else:
107
101
  e_y_mult = np.ones(n_x) * 10 ** mu_y_mult
@@ -232,7 +226,6 @@ class Mep:
232
226
  mep_min_threshold : float, optional
233
227
  Minimum user defined MEP amplitude (values below it will be filtered out).
234
228
  """
235
-
236
229
  self.intensities_orig = intensities
237
230
  self.mep_orig = mep
238
231
  self.cvar = []
@@ -330,7 +323,7 @@ class Mep:
330
323
 
331
324
  print('Fitting data to {} function ...'.format(str(self.fun.__name__)))
332
325
 
333
- # filter out unseccussful fit
326
+ # filter out unsuccessful fit
334
327
  if self.fit.covar is None:
335
328
  i_try = i_try + 1
336
329
  print('Unsuccessful fit ... trying next function!')
@@ -492,9 +485,8 @@ class Mep:
492
485
  Returns
493
486
  -------
494
487
  fit : object instance
495
- Gmodel object instance of best parameter fit with lowest parameter variance.
488
+ Gmodel object instance of the best parameter fit with the lowest parameter variance.
496
489
  """
497
-
498
490
  argnames = fun.__code__.co_varnames[1:fun.__code__.co_argcount]
499
491
  gmodel = Model(fun)
500
492
 
@@ -577,7 +569,6 @@ class Mep:
577
569
  Mep.mt : float
578
570
  Motor threshold for given MEP threshold.
579
571
  """
580
-
581
572
  self.mt = np.nan
582
573
 
583
574
  # sample MEP curve very fine in given range
@@ -592,7 +583,7 @@ class Mep:
592
583
  fontsize_axis=10, fontsize_legend=10, fontsize_label=10, fontsize_title=10, fun=None):
593
584
  """
594
585
  Plotting mep data and fitted curve together with uncertainties.
595
- If ``fun == None`, the optimal function is plotted.
586
+ If ``fun == None``, the optimal function is plotted.
596
587
 
597
588
  Parameters
598
589
  ----------
@@ -627,7 +618,8 @@ class Mep:
627
618
  p = []
628
619
  if show_plot or fname_plot:
629
620
  x_range = np.max(self.intensities) - np.min(self.intensities)
630
- x = np.linspace(np.min(self.intensities) - 0.0 * x_range, np.max(self.intensities) + 0.0 * x_range, 100)
621
+ x = np.linspace(np.min(self.intensities) - 0.0 * x_range,
622
+ np.max(self.intensities) + 0.0 * x_range, 100)
631
623
 
632
624
  # plot random sampling curves
633
625
  if plot_samples:
@@ -717,14 +709,13 @@ class Mep:
717
709
  y_max : np.ndarray of float
718
710
  (N_x) Upper bounds of y-values.
719
711
  """
720
-
721
712
  if not self.fit:
722
713
  raise Exception('Please fit function first before evaluating uncertainties!')
723
714
 
724
715
  p = [np.array([self.popt[i] - sigma * self.pstd[i], self.popt[i] + sigma * self.pstd[i]])
725
716
  for i in range(self.popt.shape[0])]
726
717
 
727
- para_combinations = pynibs.get_cartesian_product(p)
718
+ para_combinations = pynibs.util.utils.get_cartesian_product(p)
728
719
 
729
720
  y = np.zeros((x.shape[0], para_combinations.shape[0]))
730
721
 
@@ -750,7 +741,6 @@ class Mep:
750
741
  y : np.ndarray of float
751
742
  (N_x) Function values.
752
743
  """
753
-
754
744
  y = self.fun(x, *self.popt)
755
745
  return y
756
746
 
@@ -770,7 +760,6 @@ class Mep:
770
760
  y: np.ndarray of float
771
761
  (N_x) Function values.
772
762
  """
773
-
774
763
  y = self.fun(x, *p)
775
764
  return y
776
765
 
@@ -790,348 +779,10 @@ class Mep:
790
779
  y: np.ndarray of float
791
780
  (N_x) Function values.
792
781
  """
793
-
794
782
  y = self.fun_sig(x, *p)
795
783
  return y
796
784
 
797
785
 
798
- def read_biosig_emg_data(fn_data, include_first_trigger=False, type="cfs"):
799
- """
800
- Reads EMG data from a biosig file.
801
-
802
- Parameters
803
- ----------
804
- fn_data : str
805
- Path to the biosig file.
806
- include_first_trigger : bool, default: False
807
- Whether to include the first trigger event in the data (default: False).
808
- type : str, default: 'cfs'
809
- Type of the biosig file.
810
-
811
- Returns
812
- -------
813
- emg_data : np.ndarray
814
- (num_sweeps, num_channels, samples_per_sweep) EMG data with shape.
815
- time_diff_list : list
816
- Time differences between trigger events in seconds.
817
- num_sweeps : int
818
- Number of sweeps in the EMG data.
819
- num_channels : int
820
- Number of channels in the EMG data.
821
- samples_per_sweep : int
822
- Number of samples per sweep in the EMG data.
823
- sampling_rate : int
824
- Sampling rate of the EMG data.
825
- """
826
- try:
827
- import biosig
828
- except ImportError:
829
- ImportError("Please install biosig from pynibs/pkg/biosig folder!")
830
- return
831
-
832
- if type == "cfs": # TODO: also move the TXT reader here?
833
- cfs_fn = fn_data
834
- cfs_header = json.loads(biosig.header(cfs_fn))
835
- cfs_emg = biosig.data(cfs_fn)
836
-
837
- num_sweeps = cfs_header["NumberOfSweeps"]
838
- num_channels = cfs_emg.shape[1]
839
-
840
- total_num_samples = cfs_header["NumberOfSamples"]
841
- samples_per_sweep = int(total_num_samples / num_sweeps)
842
- sampling_rate = int(cfs_header["Samplingrate"])
843
-
844
- # get timestamps
845
- tms_pulse_timedelta = datetime.timedelta()
846
- # get hour, minute and second
847
- time_mep_list = []
848
- time_diff_list = []
849
- trigger_event_idcs = []
850
- if include_first_trigger:
851
- trigger_event_idcs.append(0)
852
- time_diff_list.append(0)
853
- time_mep_list.append(
854
- datetime.datetime.strptime(
855
- cfs_header["EVENT"][0]["TimeStamp"], '%Y-%b-%d %H:%M:%S'
856
- )
857
- -
858
- datetime.timedelta(
859
- seconds=float(cfs_header["EVENT"][0]["POS"])
860
- )
861
- )
862
-
863
- # convert time string into integer
864
- for event in cfs_header["EVENT"]:
865
- date = datetime.datetime.strptime(event["TimeStamp"], '%Y-%b-%d %H:%M:%S')
866
-
867
- # we are interested in the tms pulse time, so add it to ts
868
- date += tms_pulse_timedelta
869
- time_mep_list.append(date)
870
- time_diff_list.append((date - time_mep_list[0]).total_seconds())
871
-
872
- # compute indices in data block corresponding to the events
873
- if event["TYP"] == "0x7ffe":
874
- trigger_event_idcs.append(
875
- round(event["POS"] * cfs_header["Samplingrate"])
876
- )
877
-
878
- num_sweeps = min(num_sweeps, len(trigger_event_idcs))
879
-
880
- emg_data = np.zeros((num_sweeps, num_channels, samples_per_sweep), dtype=np.float32)
881
-
882
- for c_idx in range(num_channels):
883
- # Use emg data starting from the index of the first trigger event
884
- # assumptions:
885
- # - after an initial offset all emg data were captured consecutively
886
- # - the first emg data frame may be captured without an explicit TMS
887
- # tigger (eg. by checking the "write to disk" option)
888
- # - if we had dropouts in between the emg data block (not just at the
889
- # beginning) we would need to adhere to the entire trigger_event_indices
890
- # list.
891
- emg_data[:, c_idx, :] = np.reshape(
892
- cfs_emg[trigger_event_idcs[0]:, c_idx],
893
- (num_sweeps, samples_per_sweep)
894
- )
895
-
896
- return emg_data, time_diff_list, num_sweeps, num_channels, samples_per_sweep, sampling_rate
897
-
898
-
899
- def get_mep_elements(mep_fn, tms_pulse_time, drop_mep_idx=None, cfs_data_column=0, channels=None, time_format="delta",
900
- plot=False, start_mep=18, end_mep=35):
901
- """
902
- Read EMG data from CED .cfs or .txt file and returns MEP amplitudes.
903
-
904
- Parameters
905
- ----------
906
- mep_fn : string
907
- path to .cfs-file or .txt file (Signal export).
908
- tms_pulse_time : float
909
- Time in [s] of TMS pulse as specified in signal.
910
- drop_mep_idx : List of int or None, optional
911
- Which MEPs to remove before matching.
912
- cfs_data_column : int or list of int, default: 0
913
- Column(s) of dataset in cfs file. +1 for .txt.
914
- channels : list of str, optional
915
- Channel names.
916
- time_format : str, default: "delta"
917
- Format of mep time stamps in time_mep_lst to return.
918
-
919
- * ``"delta"`` returns list of datetime.timedelta in seconds.
920
- * ``"hms"`` returns datetime.datetime(year, month, day, hour, minute, second, microsecond).
921
-
922
- plot : bool, default: False
923
- Plot MEPs.
924
- start_mep : float, default: 18
925
- Start of time frame after TMS pulse where p2p value is evaluated (in ms).
926
- end_mep : float, default: 35
927
- End of time frame after TMS pulse where p2p value is evaluated (in ms).
928
-
929
- Returns
930
- -------
931
- p2p_array : np.ndarray of float
932
- (N_stim) Peak to peak values of N sweeps.
933
- time_mep_lst : list of datetime.timedelta
934
- MEP-timestamps
935
- mep_raw_data : np.ndarray of float
936
- (N_channel, N_stim, N_samples) Raw (unfiltered) MEP data.
937
- mep_filt_data : np.ndarray of float
938
- (N_channel, N_stim, N_samples) Filtered MEP data (Butterworth lowpass filter).
939
- time : np.ndarray of float
940
- (N_samples) Time axis corresponding to MEP data.
941
- mep_onset_array : np.ndarray of float
942
- (S_samples) MEP onset after TMS pulse.
943
- """
944
- # convert pulse time to datetime object in case of "delta"
945
- if time_format == "delta":
946
- tms_pulse_timedelta = datetime.timedelta(milliseconds=tms_pulse_time * 1000)
947
- elif time_format == "hms":
948
- tms_pulse_timedelta = datetime.timedelta()
949
- else:
950
- raise NotImplementedError("Specified time_format not implemented yet...")
951
-
952
- if mep_fn.endswith('.cfs'):
953
- # get data from cfs file
954
- import biosig
955
- mep_raw_data_tmp = biosig.data(mep_fn)
956
- mep_raw_data_tmp = mep_raw_data_tmp[:, cfs_data_column] # get first channel
957
-
958
- # get header from cfs file
959
- cfs_header = biosig.header(mep_fn)
960
-
961
- # get timestamps
962
- # get all indices of timestamps from cfs header
963
- ts_mep_lst = [timestamp.start() for timestamp in re.finditer('TimeStamp', cfs_header)]
964
- # get hour, minute and second
965
- time_mep_list = []
966
- # convert time string into integer
967
- for index in ts_mep_lst:
968
- hour = int(cfs_header[index + 26:index + 28])
969
- minute = int(cfs_header[index + 29:index + 31])
970
- second = int(cfs_header[index + 32:index + 34])
971
- # fix bug with second 60
972
- if second == 60:
973
- ts = datetime.datetime(1900, 1, 1, hour, minute, 59)
974
- ts += datetime.timedelta(seconds=1)
975
- else:
976
- ts = datetime.datetime(1900, 1, 1, hour, minute, second)
977
-
978
- # we are interested in the tms pulse time, so add it to ts
979
- ts += tms_pulse_timedelta
980
- time_mep_list.append(ts)
981
-
982
- if time_format == "delta":
983
- time_mep_list = [time_mep_list[i] - time_mep_list[0] for i in range(len(time_mep_list))]
984
- if time_format == "hms":
985
- pass
986
-
987
- # add first timestamp (not saved by signal) and shift other by isi
988
- time_mep_list = [datetime.timedelta(seconds=0)] + [t + time_mep_list[1] - time_mep_list[0] for t in
989
- time_mep_list]
990
-
991
- # get peak-to-peak values
992
- # get the ratio of samples per sweep
993
- sweep_index = cfs_header.find('NumberOfSweeps')
994
- comma_index = cfs_header.find(',', sweep_index)
995
- n_sweeps = int(cfs_header[sweep_index + 18:comma_index])
996
- record_index = cfs_header.find('NumberOfRecords')
997
- comma_index = cfs_header.find(',', record_index)
998
- records = int(cfs_header[record_index + 19:comma_index])
999
- n_samples = int(records / n_sweeps)
1000
- if not isinstance(n_samples, int):
1001
- print('Warning: Number of samples is not an integer.')
1002
- # TODO: Correct get_mep_elements() sample number check. This does not work as expected (from Ole)
1003
-
1004
- # reshape numpy array
1005
- mep_raw_arr = np.zeros((len(cfs_data_column), n_sweeps, n_samples))
1006
-
1007
- for i in cfs_data_column:
1008
- mep_raw_arr[i, :, :] = np.reshape(mep_raw_data_tmp[:, i], (n_sweeps, n_samples))
1009
-
1010
- sampling_rate = get_mep_sampling_rate(mep_fn)
1011
-
1012
- elif mep_fn.endswith('.mat'):
1013
- mep_data = spio.loadmat(mep_fn, struct_as_record=False, squeeze_me=True)
1014
-
1015
- # find data
1016
- for k in mep_data.keys():
1017
- if isinstance(mep_data[k], spio.matlab.mio5_params.mat_struct):
1018
- mep_data = mep_data[k].__dict__
1019
- break
1020
-
1021
- n_samples = mep_data['points']
1022
- mep_raw_arr = mep_data['values'].transpose(1, 2, 0)
1023
- time_mep_list = [datetime.timedelta(seconds=f.__dict__['start']) for f in mep_data['frameinfo']]
1024
- sampling_rate = get_mep_sampling_rate(mep_fn)
1025
-
1026
- elif mep_fn.endswith('.txt'):
1027
- warnings.warn(".txt import is deprecated - use .mat or .cfs.", DeprecationWarning)
1028
- print("Reading MEP from .txt file")
1029
- # The Signal text export looks like this:
1030
- #
1031
- # "s"\t"ADC 0"\t"ADC 1"
1032
- # 0.00000000\t-0.066681\t-0.047607
1033
- # 0.00025000\t-0.066376\t-0.049286
1034
- # 0.00050000\t-0.066528\t-0.056610
1035
- #
1036
- # "s"\t"ADC 0"\t"ADC 1"
1037
- # 0.00000000\t-0.066681\t-0.047607
1038
- # 0.00025000\t-0.066376\t-0.049286
1039
- # 0.00050000\t-0.066528\t-0.056610
1040
- #
1041
- # With first column = time, second = 1st electrode, ...
1042
- # This is an example of 2 sweeps, 3 samples each, sampling rate = 4000
1043
-
1044
- # Find number of samples per sweep
1045
- pattern = '"s"'
1046
- with open(mep_fn, 'r') as f:
1047
- for line_nr, line in enumerate(f):
1048
- print(f'{line_nr}: {line}')
1049
- if pattern in line and line_nr > 0:
1050
- # find second occurance of "s" -> end of first sweep
1051
- n_samples = line_nr
1052
- print(f'{line_nr}: {line}')
1053
- if line != '\n':
1054
- last_sample_time = line
1055
-
1056
- # extract time (first column) of last samples
1057
- last_sample_time = float(last_sample_time[0:last_sample_time.find('\t')])
1058
-
1059
- # subtract 2 because first row is header ("s"\t"ADC 0"\t"ADC 1") and last row is blank
1060
- n_samples = n_samples - 2
1061
-
1062
- df_mep = pd.read_csv(mep_fn,
1063
- delimiter="\t",
1064
- skip_blank_lines=True,
1065
- skiprows=lambda x: x % (n_samples + 2) == 0 and x > 0)
1066
-
1067
- n_sweeps = int(df_mep.shape[0] / n_samples)
1068
- mep_raw_arr = np.zeros((len(cfs_data_column), n_sweeps, n_samples))
1069
-
1070
- for i in range(n_sweeps):
1071
- mep_raw_arr[:, i, :] = df_mep.iloc[i * n_samples:(i + 1) * n_samples, 1:].transpose()
1072
-
1073
- # get sampling rate by dividing number of sweeps by timing
1074
- sampling_rate = int(mep_raw_arr.shape[2] - 1) / last_sample_time
1075
-
1076
- # build time_mep_list
1077
- # we only have information about the single mep timings, so let's assume signal sticks strictly to the protocol
1078
- sample_len = last_sample_time + 1 / sampling_rate
1079
-
1080
- # TODO: The ISI is missing here, do we want to add it to the subject object?
1081
- time_mep_list = [datetime.timedelta(seconds=i * sample_len) +
1082
- tms_pulse_timedelta for i in range(mep_raw_arr.shape[1])]
1083
-
1084
- else:
1085
- raise ValueError("Unknown MEP file extension. Use .cfs or .txt.")
1086
-
1087
- # get peak to peak value of every sweep and plot results in mep/plots/channels
1088
- if channels is None:
1089
- channels = [str(i) for i in cfs_data_column]
1090
-
1091
- tmp = np.zeros((mep_raw_arr.shape[0], mep_raw_arr.shape[1], 3)).astype(object)
1092
- for i_channel in range(mep_raw_arr.shape[0]):
1093
- print(f"Calculating p2p values for channel: {channels[i_channel]}")
1094
-
1095
- for i_zap in range(mep_raw_arr.shape[1]):
1096
- tmp[i_channel, i_zap, 0], \
1097
- tmp[i_channel, i_zap, 1], \
1098
- tmp[i_channel, i_zap, 2] = calc_p2p(sweep=mep_raw_arr[i_channel, i_zap, :],
1099
- tms_pulse_time=tms_pulse_time,
1100
- sampling_rate=sampling_rate,
1101
- fn_plot=None,
1102
- start_mep=start_mep,
1103
- end_mep=end_mep)
1104
-
1105
- p2p_arr = np.zeros((tmp.shape[0], tmp.shape[1]))
1106
- mep_onset_arr = np.zeros((tmp.shape[0], tmp.shape[1]))
1107
- mep_filt_arr = np.zeros(mep_raw_arr.shape)
1108
-
1109
- time = np.arange(mep_raw_arr.shape[2]) / sampling_rate
1110
-
1111
- for idx_channel in cfs_data_column:
1112
- for i, t in enumerate(tmp[idx_channel, :, :]):
1113
- p2p_arr[idx_channel, i] = tmp[idx_channel, i, 0]
1114
- mep_onset_arr[idx_channel, i] = tmp[idx_channel, i, 2]
1115
- mep_filt_arr[idx_channel, i, :] = tmp[idx_channel, i, 1]
1116
-
1117
- if time_format == "delta":
1118
- time_mep_list = [time_mep_list[i] - time_mep_list[0] for i in range(len(time_mep_list))]
1119
- elif time_format == "hms":
1120
- pass
1121
-
1122
- # remove MEPs according to drop_mep_idx and reset time
1123
- if drop_mep_idx is not None:
1124
- p2p_arr = np.delete(p2p_arr, drop_mep_idx)
1125
- mep_onset_arr = np.delete(mep_onset_arr, drop_mep_idx)
1126
- time_mep_list = np.delete(time_mep_list, drop_mep_idx)
1127
-
1128
- keep_mep_idx = [i for i in range(mep_raw_arr.shape[1]) if i not in np.array(drop_mep_idx)]
1129
- mep_raw_arr = mep_raw_arr[:, keep_mep_idx, :]
1130
- mep_filt_arr = mep_filt_arr[:, keep_mep_idx, :]
1131
-
1132
- return p2p_arr, time_mep_list, mep_raw_arr, mep_filt_arr, time, mep_onset_arr
1133
-
1134
-
1135
786
  def calc_p2p_old_exp0(sweep, start_mep=None, end_mep=None, tms_pulse_time=None, sampling_rate=None):
1136
787
  """
1137
788
  Calc peak-to-peak values of an mep sweep.
@@ -1260,6 +911,48 @@ def calc_p2p_old_exp1(sweep, start_mep=18, end_mep=35, tms_pulse_time=None, samp
1260
911
 
1261
912
  return sweep_max - sweep_min
1262
913
 
914
+ def calc_emg_preactivation(sweep, tms_pulse_time=.2, measurement_start_time=0,
915
+ sampling_rate=4000):
916
+ """
917
+ Computes EMG preactivation, i.e. max-min and SD of the EMG timeseries before the TMS pulse.
918
+
919
+ Parameters
920
+ ----------
921
+ sweep : np.ndarray of float
922
+ (Nx1) Input curve.
923
+ tms_pulse_time : float, default: .2
924
+ Onset time of TMS pulse trigger in [s].
925
+ measurement_start_time : float, default: 0
926
+ Start time of the EMG measurement in [ms].
927
+ sampling_rate : int, default: 2000
928
+ Sampling rate in Hz.
929
+
930
+ Returns
931
+ -------
932
+ maxmin : float
933
+ The max-min of EMG timeseries before TMS pulse
934
+ sd : float
935
+ The max-min of EMG timeseries before TMS pulse
936
+ """
937
+ def time_to_idx_conversion(t):
938
+ return int((t - measurement_start_time) * sampling_rate / 1000)
939
+
940
+ def idx_to_time_conversion(i):
941
+ return i * 1000 / sampling_rate + measurement_start_time
942
+
943
+ # Compute start and stop idx according to sampling rate
944
+ if tms_pulse_time > 1:
945
+ warnings.warn(f"Is tms_pulse_time={tms_pulse_time} really in seconds?")
946
+
947
+ srch_win_end = time_to_idx_conversion(tms_pulse_time * 1000) # get TMS impulse
948
+
949
+ if srch_win_end >= sweep.size:
950
+ srch_win_end = sweep.size - 1
951
+
952
+ maxmin = sweep[:srch_win_end].max() - sweep[:srch_win_end].min()
953
+
954
+ sd = np.var(sweep[:srch_win_end])
955
+ return maxmin, sd
1263
956
 
1264
957
  def calc_p2p(sweep, tms_pulse_time=.2, start_mep=20, end_mep=35, measurement_start_time=0, sampling_rate=4000,
1265
958
  cutoff_freq=500, fn_plot=None):
@@ -1294,7 +987,6 @@ def calc_p2p(sweep, tms_pulse_time=.2, start_mep=20, end_mep=35, measurement_sta
1294
987
  onset : float
1295
988
  MEP onset after tms_pulse_time.
1296
989
  """
1297
-
1298
990
  def time_to_idx_conversion(t):
1299
991
  return int((t - measurement_start_time) * sampling_rate / 1000)
1300
992
 
@@ -1307,9 +999,9 @@ def calc_p2p(sweep, tms_pulse_time=.2, start_mep=20, end_mep=35, measurement_sta
1307
999
 
1308
1000
  # Filter requirements.
1309
1001
  order = 6
1310
-
1311
1002
  sweep_filt = butter_lowpass_filter(sweep, cutoff_freq, sampling_rate, order)
1312
-
1003
+ # sweep_filt = butter_highpass_filter(sweep, 5, sampling_rate, order)
1004
+ # sweep_filt = sweep.copy()
1313
1005
  # beginning of mep search window
1314
1006
  srch_win_start = time_to_idx_conversion(tms_pulse_time * 1000 + start_mep) # get TMS impulse
1315
1007
 
@@ -1346,15 +1038,26 @@ def calc_p2p(sweep, tms_pulse_time=.2, start_mep=20, end_mep=35, measurement_sta
1346
1038
  plt.scatter(timepoints[sweep_max_idx], sweep_max, 15, color="r")
1347
1039
  plt.plot(timepoints, np.ones(len(timepoints)) * sweep_min, linestyle="--", color="r", linewidth=1)
1348
1040
  plt.plot(timepoints, np.ones(len(timepoints)) * sweep_max, linestyle="--", color="r", linewidth=1)
1041
+ plt.vlines(x=timepoints[srch_win_start], ymin=sweep.min(), ymax=sweep.max(), color="r", linewidth=1)
1042
+ plt.vlines(x=timepoints[srch_win_end], ymin=sweep.min(), ymax=sweep.max(), color="r", linewidth=1)
1349
1043
  plt.grid()
1350
1044
  plt.legend(["raw", "filtered", "p2p"], loc='upper right')
1045
+ # plt.show()
1046
+ # plt.close()
1351
1047
 
1352
1048
  plt.xlim([np.max((tms_pulse_time - 0.01, np.min(timepoints))),
1353
1049
  np.min((timepoints[srch_win_end] + .1, np.max(timepoints)))])
1050
+
1051
+ # let's have a minimum of 50µV for the plot
1052
+ diff = np.abs(sweep_min) - np.abs(sweep_max)
1053
+ if diff < .05:
1054
+ sweep_min -= .025
1055
+ sweep_max += .025
1354
1056
  plt.ylim([-1.1 * np.abs(sweep_min), 1.1 * np.abs(sweep_max)])
1355
1057
 
1356
1058
  plt.xlabel("time in s", fontsize=11)
1357
1059
  plt.ylabel("MEP in mV", fontsize=11)
1060
+ plt.title(f"MEP: {p2p:.2f} mV", fontsize=11)
1358
1061
  plt.tight_layout()
1359
1062
 
1360
1063
  plt.savefig(fn_plot, dpi=300, transparent=True)
@@ -1371,7 +1074,8 @@ def get_mep_sampling_rate(cfs_path):
1371
1074
 
1372
1075
  .. code-block:: sh
1373
1076
 
1374
- ``Samplingrate"\t: 3999.999810,\n``
1077
+ Samplingrate\t: 3999.999810
1078
+
1375
1079
 
1376
1080
  Parameters
1377
1081
  ----------
@@ -1444,13 +1148,36 @@ def butter_lowpass(cutoff, fs, order=5):
1444
1148
  b, a : np.ndarray, np.ndarray
1445
1149
  Numerator (b) and denominator (a) polynomials of the IIR filter.
1446
1150
  """
1447
-
1448
1151
  nyq = 0.5 * fs
1449
1152
  normal_cutoff = cutoff / nyq
1450
1153
  b, a = butter(order, normal_cutoff, btype='low', analog=False)
1451
1154
  return b, a
1452
1155
 
1453
1156
 
1157
+ def butter_highpass(cutoff, fs, order=5):
1158
+ """
1159
+ Setup Butter high-pass filter and return filter parameters.
1160
+
1161
+ Parameters
1162
+ ----------
1163
+ cutoff : float
1164
+ Cutoff frequency in [Hz].
1165
+ fs : float
1166
+ Sampling frequency in [Hz].
1167
+ order : int, default: 5
1168
+ Filter order.
1169
+
1170
+ Returns
1171
+ -------
1172
+ b, a : np.ndarray, np.ndarray
1173
+ Numerator (b) and denominator (a) polynomials of the IIR filter.
1174
+ """
1175
+ nyq = 0.5 * fs
1176
+ normal_cutoff = cutoff / nyq
1177
+ b, a = butter(order, normal_cutoff, btype='high', analog=False)
1178
+ return b, a
1179
+
1180
+
1454
1181
  def butter_lowpass_filter(data, cutoff, fs, order=5):
1455
1182
  """
1456
1183
  Applies Butterworth lowpass filter.
@@ -1471,12 +1198,36 @@ def butter_lowpass_filter(data, cutoff, fs, order=5):
1471
1198
  y : np.ndarray
1472
1199
  (N_samples) Output of the digital filter.
1473
1200
  """
1474
-
1475
1201
  b, a = butter_lowpass(cutoff, fs, order=order)
1476
1202
  y = lfilter(b, a, data)
1477
1203
  return y
1478
1204
 
1479
1205
 
1206
+ def butter_highpass_filter(data, cutoff, fs, order=5):
1207
+ """
1208
+ Applies Butterworth lowpass filter.
1209
+
1210
+ Parameters
1211
+ ----------
1212
+ data : np.ndarray of float
1213
+ (N_samples) Input of the digital filter.
1214
+ cutoff : float
1215
+ Cutoff frequency in [Hz].
1216
+ fs : float
1217
+ Sampling frequency in [Hz].
1218
+ order : int
1219
+ Filter order.
1220
+
1221
+ Returns
1222
+ -------
1223
+ y : np.ndarray
1224
+ (N_samples) Output of the digital filter.
1225
+ """
1226
+ b, a = butter_highpass(cutoff, fs, order=order)
1227
+ y = lfilter(b, a, data)
1228
+ return y
1229
+
1230
+
1480
1231
  def get_time_date(cfs_paths):
1481
1232
  """
1482
1233
  Get time and date of the start of the recording out of .cfs file.
@@ -1491,7 +1242,6 @@ def get_time_date(cfs_paths):
1491
1242
  time_date : str
1492
1243
  Date and time.
1493
1244
  """
1494
-
1495
1245
  cfs_header = biosig.header(cfs_paths[0])
1496
1246
  index = cfs_header.find('StartOfRecording')
1497
1247
  time_date = cfs_header[index + 21:index + 40]
@@ -1515,4 +1265,4 @@ def scale_e_for_dvs(e, mt=60, upper_limit=100):
1515
1265
  -------
1516
1266
  float : scaled electric field in V/m.
1517
1267
  """
1518
- return (e - mt)/(upper_limit - mt)
1268
+ return (e - mt)/(upper_limit - mt)
pynibs/expio/__init__.py CHANGED
@@ -1,8 +1,18 @@
1
+ import pynibs
1
2
  from .brainvis import *
2
3
  from .brainsight import *
3
4
  from .exp import *
4
5
  from .fit_funs import *
6
+ from .invesalius import *
5
7
  from .localite import *
6
8
  from .Mep import *
7
9
  from .signal_ced import *
8
10
  from .visor import *
11
+ import warnings
12
+
13
+
14
+ def get_mep_elements(*args, **kwargs):
15
+ warnings.warn(
16
+ "pyhibs.get_mep_elements() is deprecated, use pynibs.expio.signal_ced.get_mep_elements()"
17
+ " or pynibs.expio.neurone.get_mep_elements() instead.", DeprecationWarning, stacklevel=2)
18
+ return pynibs.expio.signal_ced.get_mep_elements(*args, **kwargs)