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.
- pynibs/__init__.py +26 -14
- pynibs/coil/__init__.py +6 -0
- pynibs/{coil.py → coil/coil.py} +213 -543
- pynibs/coil/export.py +508 -0
- pynibs/congruence/__init__.py +4 -1
- pynibs/congruence/congruence.py +37 -45
- pynibs/congruence/ext_metrics.py +40 -11
- pynibs/congruence/stimulation_threshold.py +1 -2
- pynibs/expio/Mep.py +120 -370
- pynibs/expio/__init__.py +10 -0
- pynibs/expio/brainsight.py +34 -37
- pynibs/expio/cobot.py +25 -25
- pynibs/expio/exp.py +10 -7
- pynibs/expio/fit_funs.py +3 -0
- pynibs/expio/invesalius.py +70 -0
- pynibs/expio/localite.py +190 -91
- pynibs/expio/neurone.py +139 -0
- pynibs/expio/signal_ced.py +345 -2
- pynibs/expio/visor.py +16 -15
- pynibs/freesurfer.py +34 -33
- pynibs/hdf5_io/hdf5_io.py +149 -132
- pynibs/hdf5_io/xdmf.py +35 -31
- pynibs/mesh/__init__.py +1 -1
- pynibs/mesh/mesh_struct.py +77 -92
- pynibs/mesh/transformations.py +121 -21
- pynibs/mesh/utils.py +191 -99
- pynibs/models/_TMS.py +2 -1
- pynibs/muap.py +1 -2
- pynibs/neuron/__init__.py +10 -0
- pynibs/neuron/models/mep.py +566 -0
- pynibs/neuron/neuron_regression.py +98 -8
- pynibs/optimization/__init__.py +12 -2
- pynibs/optimization/{optimization.py → coil_opt.py} +157 -133
- pynibs/optimization/multichannel.py +1174 -24
- pynibs/optimization/workhorses.py +7 -8
- pynibs/regression/__init__.py +4 -2
- pynibs/regression/dual_node_detection.py +229 -219
- pynibs/regression/regression.py +92 -61
- pynibs/roi/__init__.py +4 -1
- pynibs/roi/roi_structs.py +19 -21
- pynibs/roi/{roi.py → roi_utils.py} +56 -33
- pynibs/subject.py +24 -14
- pynibs/util/__init__.py +20 -4
- pynibs/util/dosing.py +4 -5
- pynibs/util/quality_measures.py +39 -38
- pynibs/util/rotations.py +116 -9
- pynibs/util/{simnibs.py → simnibs_io.py} +29 -19
- pynibs/util/{util.py → utils.py} +20 -22
- pynibs/visualization/para.py +4 -4
- pynibs/visualization/render_3D.py +4 -4
- pynibs-0.2026.1.dist-info/METADATA +105 -0
- pynibs-0.2026.1.dist-info/RECORD +69 -0
- {pyNIBS-0.2024.8.dist-info → pynibs-0.2026.1.dist-info}/WHEEL +1 -1
- pyNIBS-0.2024.8.dist-info/METADATA +0 -723
- pyNIBS-0.2024.8.dist-info/RECORD +0 -107
- pynibs/data/configuration_exp0.yaml +0 -59
- pynibs/data/configuration_linear_MEP.yaml +0 -61
- pynibs/data/configuration_linear_RT.yaml +0 -61
- pynibs/data/configuration_sigmoid4.yaml +0 -68
- pynibs/data/network mapping configuration/configuration guide.md +0 -238
- pynibs/data/network mapping configuration/configuration_TEMPLATE.yaml +0 -42
- pynibs/data/network mapping configuration/configuration_for_testing.yaml +0 -43
- pynibs/data/network mapping configuration/configuration_modelTMS.yaml +0 -43
- pynibs/data/network mapping configuration/configuration_reg_isi_05.yaml +0 -43
- pynibs/data/network mapping configuration/output_documentation.md +0 -185
- pynibs/data/network mapping configuration/recommendations_for_accuracy_threshold.md +0 -77
- pynibs/data/neuron/models/L23_PC_cADpyr_biphasic_v1.csv +0 -1281
- pynibs/data/neuron/models/L23_PC_cADpyr_monophasic_v1.csv +0 -1281
- pynibs/data/neuron/models/L4_LBC_biphasic_v1.csv +0 -1281
- pynibs/data/neuron/models/L4_LBC_monophasic_v1.csv +0 -1281
- pynibs/data/neuron/models/L4_NBC_biphasic_v1.csv +0 -1281
- pynibs/data/neuron/models/L4_NBC_monophasic_v1.csv +0 -1281
- pynibs/data/neuron/models/L4_SBC_biphasic_v1.csv +0 -1281
- pynibs/data/neuron/models/L4_SBC_monophasic_v1.csv +0 -1281
- pynibs/data/neuron/models/L5_TTPC2_cADpyr_biphasic_v1.csv +0 -1281
- pynibs/data/neuron/models/L5_TTPC2_cADpyr_monophasic_v1.csv +0 -1281
- pynibs/tests/data/InstrumentMarker20200225163611937.xml +0 -19
- pynibs/tests/data/TriggerMarkers_Coil0_20200225163443682.xml +0 -14
- pynibs/tests/data/TriggerMarkers_Coil1_20200225170337572.xml +0 -6373
- pynibs/tests/data/Xdmf.dtd +0 -89
- pynibs/tests/data/brainsight_niiImage_nifticoord.txt +0 -145
- pynibs/tests/data/brainsight_niiImage_nifticoord_largefile.txt +0 -1434
- pynibs/tests/data/brainsight_niiImage_niifticoord_mixedtargets.txt +0 -47
- pynibs/tests/data/create_subject_testsub.py +0 -332
- pynibs/tests/data/data.hdf5 +0 -0
- pynibs/tests/data/geo.hdf5 +0 -0
- pynibs/tests/test_coil.py +0 -474
- pynibs/tests/test_elements2nodes.py +0 -100
- pynibs/tests/test_hdf5_io/test_xdmf.py +0 -61
- pynibs/tests/test_mesh_transformations.py +0 -123
- pynibs/tests/test_mesh_utils.py +0 -143
- pynibs/tests/test_nnav_imports.py +0 -101
- pynibs/tests/test_quality_measures.py +0 -117
- pynibs/tests/test_regressdata.py +0 -289
- pynibs/tests/test_roi.py +0 -17
- pynibs/tests/test_rotations.py +0 -86
- pynibs/tests/test_subject.py +0 -71
- pynibs/tests/test_util.py +0 -24
- /pynibs/{regression/score_types.py → neuron/models/m1_montbrio.py} +0 -0
- {pyNIBS-0.2024.8.dist-info → pynibs-0.2026.1.dist-info/licenses}/LICENSE +0 -0
- {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(
|
|
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,
|
|
101
|
-
|
|
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),
|
|
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
|
|
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
|
|
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,
|
|
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
|
-
|
|
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)
|