py-neuromodulation 0.0.4__py3-none-any.whl → 0.0.6__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.
- py_neuromodulation/ConnectivityDecoding/_get_grid_hull.m +34 -34
- py_neuromodulation/ConnectivityDecoding/_get_grid_whole_brain.py +95 -106
- py_neuromodulation/ConnectivityDecoding/_helper_write_connectome.py +107 -119
- py_neuromodulation/__init__.py +80 -13
- py_neuromodulation/{nm_RMAP.py → analysis/RMAP.py} +496 -531
- py_neuromodulation/analysis/__init__.py +4 -0
- py_neuromodulation/{nm_decode.py → analysis/decode.py} +918 -992
- py_neuromodulation/{nm_analysis.py → analysis/feature_reader.py} +994 -1074
- py_neuromodulation/{nm_plots.py → analysis/plots.py} +627 -612
- py_neuromodulation/{nm_stats.py → analysis/stats.py} +458 -480
- py_neuromodulation/data/README +6 -6
- py_neuromodulation/data/dataset_description.json +8 -8
- py_neuromodulation/data/participants.json +32 -32
- py_neuromodulation/data/participants.tsv +2 -2
- py_neuromodulation/data/sub-testsub/ses-EphysMedOff/ieeg/sub-testsub_ses-EphysMedOff_space-mni_coordsystem.json +5 -5
- py_neuromodulation/data/sub-testsub/ses-EphysMedOff/ieeg/sub-testsub_ses-EphysMedOff_space-mni_electrodes.tsv +11 -11
- py_neuromodulation/data/sub-testsub/ses-EphysMedOff/ieeg/sub-testsub_ses-EphysMedOff_task-gripforce_run-0_channels.tsv +11 -11
- py_neuromodulation/data/sub-testsub/ses-EphysMedOff/ieeg/sub-testsub_ses-EphysMedOff_task-gripforce_run-0_ieeg.json +18 -18
- py_neuromodulation/data/sub-testsub/ses-EphysMedOff/ieeg/sub-testsub_ses-EphysMedOff_task-gripforce_run-0_ieeg.vhdr +35 -35
- py_neuromodulation/data/sub-testsub/ses-EphysMedOff/ieeg/sub-testsub_ses-EphysMedOff_task-gripforce_run-0_ieeg.vmrk +13 -13
- py_neuromodulation/data/sub-testsub/ses-EphysMedOff/sub-testsub_ses-EphysMedOff_scans.tsv +2 -2
- py_neuromodulation/default_settings.yaml +241 -0
- py_neuromodulation/features/__init__.py +31 -0
- py_neuromodulation/features/bandpower.py +165 -0
- py_neuromodulation/features/bispectra.py +157 -0
- py_neuromodulation/features/bursts.py +297 -0
- py_neuromodulation/features/coherence.py +255 -0
- py_neuromodulation/features/feature_processor.py +121 -0
- py_neuromodulation/features/fooof.py +142 -0
- py_neuromodulation/features/hjorth_raw.py +57 -0
- py_neuromodulation/features/linelength.py +21 -0
- py_neuromodulation/features/mne_connectivity.py +148 -0
- py_neuromodulation/features/nolds.py +94 -0
- py_neuromodulation/features/oscillatory.py +249 -0
- py_neuromodulation/features/sharpwaves.py +432 -0
- py_neuromodulation/filter/__init__.py +3 -0
- py_neuromodulation/filter/kalman_filter.py +67 -0
- py_neuromodulation/filter/kalman_filter_external.py +1890 -0
- py_neuromodulation/filter/mne_filter.py +128 -0
- py_neuromodulation/filter/notch_filter.py +93 -0
- py_neuromodulation/grid_cortex.tsv +40 -40
- py_neuromodulation/liblsl/libpugixml.so.1.12 +0 -0
- py_neuromodulation/liblsl/linux/bionic_amd64/liblsl.1.16.2.so +0 -0
- py_neuromodulation/liblsl/linux/bookworm_amd64/liblsl.1.16.2.so +0 -0
- py_neuromodulation/liblsl/linux/focal_amd46/liblsl.1.16.2.so +0 -0
- py_neuromodulation/liblsl/linux/jammy_amd64/liblsl.1.16.2.so +0 -0
- py_neuromodulation/liblsl/linux/jammy_x86/liblsl.1.16.2.so +0 -0
- py_neuromodulation/liblsl/linux/noble_amd64/liblsl.1.16.2.so +0 -0
- py_neuromodulation/liblsl/macos/amd64/liblsl.1.16.2.dylib +0 -0
- py_neuromodulation/liblsl/macos/arm64/liblsl.1.16.0.dylib +0 -0
- py_neuromodulation/liblsl/windows/amd64/liblsl.1.16.2.dll +0 -0
- py_neuromodulation/liblsl/windows/x86/liblsl.1.16.2.dll +0 -0
- py_neuromodulation/processing/__init__.py +10 -0
- py_neuromodulation/{nm_artifacts.py → processing/artifacts.py} +29 -25
- py_neuromodulation/processing/data_preprocessor.py +77 -0
- py_neuromodulation/processing/filter_preprocessing.py +78 -0
- py_neuromodulation/processing/normalization.py +175 -0
- py_neuromodulation/{nm_projection.py → processing/projection.py} +370 -394
- py_neuromodulation/{nm_rereference.py → processing/rereference.py} +97 -95
- py_neuromodulation/{nm_resample.py → processing/resample.py} +56 -50
- py_neuromodulation/stream/__init__.py +3 -0
- py_neuromodulation/stream/data_processor.py +325 -0
- py_neuromodulation/stream/generator.py +53 -0
- py_neuromodulation/stream/mnelsl_player.py +94 -0
- py_neuromodulation/stream/mnelsl_stream.py +120 -0
- py_neuromodulation/stream/settings.py +292 -0
- py_neuromodulation/stream/stream.py +427 -0
- py_neuromodulation/utils/__init__.py +2 -0
- py_neuromodulation/{nm_define_nmchannels.py → utils/channels.py} +305 -302
- py_neuromodulation/utils/database.py +149 -0
- py_neuromodulation/utils/io.py +378 -0
- py_neuromodulation/utils/keyboard.py +52 -0
- py_neuromodulation/utils/logging.py +66 -0
- py_neuromodulation/utils/types.py +251 -0
- {py_neuromodulation-0.0.4.dist-info → py_neuromodulation-0.0.6.dist-info}/METADATA +28 -33
- py_neuromodulation-0.0.6.dist-info/RECORD +89 -0
- {py_neuromodulation-0.0.4.dist-info → py_neuromodulation-0.0.6.dist-info}/WHEEL +1 -1
- {py_neuromodulation-0.0.4.dist-info → py_neuromodulation-0.0.6.dist-info}/licenses/LICENSE +21 -21
- py_neuromodulation/FieldTrip.py +0 -589
- py_neuromodulation/_write_example_dataset_helper.py +0 -65
- py_neuromodulation/nm_EpochStream.py +0 -92
- py_neuromodulation/nm_IO.py +0 -417
- py_neuromodulation/nm_across_patient_decoding.py +0 -927
- py_neuromodulation/nm_bispectra.py +0 -168
- py_neuromodulation/nm_bursts.py +0 -198
- py_neuromodulation/nm_coherence.py +0 -205
- py_neuromodulation/nm_cohortwrapper.py +0 -435
- py_neuromodulation/nm_eval_timing.py +0 -239
- py_neuromodulation/nm_features.py +0 -116
- py_neuromodulation/nm_features_abc.py +0 -39
- py_neuromodulation/nm_filter.py +0 -219
- py_neuromodulation/nm_filter_preprocessing.py +0 -91
- py_neuromodulation/nm_fooof.py +0 -159
- py_neuromodulation/nm_generator.py +0 -37
- py_neuromodulation/nm_hjorth_raw.py +0 -73
- py_neuromodulation/nm_kalmanfilter.py +0 -58
- py_neuromodulation/nm_linelength.py +0 -33
- py_neuromodulation/nm_mne_connectivity.py +0 -112
- py_neuromodulation/nm_nolds.py +0 -93
- py_neuromodulation/nm_normalization.py +0 -214
- py_neuromodulation/nm_oscillatory.py +0 -448
- py_neuromodulation/nm_run_analysis.py +0 -435
- py_neuromodulation/nm_settings.json +0 -338
- py_neuromodulation/nm_settings.py +0 -68
- py_neuromodulation/nm_sharpwaves.py +0 -401
- py_neuromodulation/nm_stream_abc.py +0 -218
- py_neuromodulation/nm_stream_offline.py +0 -359
- py_neuromodulation/utils/_logging.py +0 -24
- py_neuromodulation-0.0.4.dist-info/RECORD +0 -72
|
@@ -1,73 +0,0 @@
|
|
|
1
|
-
import enum
|
|
2
|
-
import numpy as np
|
|
3
|
-
from typing import Iterable
|
|
4
|
-
|
|
5
|
-
from py_neuromodulation import nm_features_abc
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
class Hjorth(nm_features_abc.Feature):
|
|
9
|
-
def __init__(
|
|
10
|
-
self, settings: dict, ch_names: Iterable[str], sfreq: float
|
|
11
|
-
) -> None:
|
|
12
|
-
self.s = settings
|
|
13
|
-
self.ch_names = ch_names
|
|
14
|
-
|
|
15
|
-
@staticmethod
|
|
16
|
-
def test_settings(
|
|
17
|
-
settings: dict,
|
|
18
|
-
ch_names: Iterable[str],
|
|
19
|
-
sfreq: int | float,
|
|
20
|
-
):
|
|
21
|
-
# no settings to test
|
|
22
|
-
pass
|
|
23
|
-
|
|
24
|
-
def calc_feature(self, data: np.array, features_compute: dict) -> dict:
|
|
25
|
-
for ch_idx, ch_name in enumerate(self.ch_names):
|
|
26
|
-
features_compute[
|
|
27
|
-
"_".join([ch_name, "RawHjorth_Activity"])
|
|
28
|
-
] = np.nan_to_num(np.var(data[ch_idx, :]))
|
|
29
|
-
deriv_variance = np.nan_to_num(np.var(np.diff(data[ch_idx, :])))
|
|
30
|
-
mobility = np.nan_to_num(
|
|
31
|
-
np.sqrt(deriv_variance / np.var(data[ch_idx, :]))
|
|
32
|
-
)
|
|
33
|
-
features_compute[
|
|
34
|
-
"_".join([ch_name, "RawHjorth_Mobility"])
|
|
35
|
-
] = mobility
|
|
36
|
-
|
|
37
|
-
dat_deriv_2_var = np.nan_to_num(
|
|
38
|
-
np.var(np.diff(np.diff(data[ch_idx, :])))
|
|
39
|
-
)
|
|
40
|
-
deriv_mobility = np.nan_to_num(
|
|
41
|
-
np.sqrt(dat_deriv_2_var / deriv_variance)
|
|
42
|
-
)
|
|
43
|
-
features_compute[
|
|
44
|
-
"_".join([ch_name, "RawHjorth_Complexity"])
|
|
45
|
-
] = np.nan_to_num(deriv_mobility / mobility)
|
|
46
|
-
|
|
47
|
-
return features_compute
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
class Raw(nm_features_abc.Feature):
|
|
51
|
-
def __init__(
|
|
52
|
-
self, settings: dict, ch_names: Iterable[str], sfreq: float
|
|
53
|
-
) -> None:
|
|
54
|
-
self.ch_names = ch_names
|
|
55
|
-
|
|
56
|
-
def calc_feature(
|
|
57
|
-
self,
|
|
58
|
-
data: np.array,
|
|
59
|
-
features_compute: dict,
|
|
60
|
-
) -> dict:
|
|
61
|
-
for ch_idx, ch_name in enumerate(self.ch_names):
|
|
62
|
-
features_compute["_".join([ch_name, "raw"])] = data[ch_idx, -1]
|
|
63
|
-
|
|
64
|
-
return features_compute
|
|
65
|
-
|
|
66
|
-
@staticmethod
|
|
67
|
-
def test_settings(
|
|
68
|
-
settings: dict,
|
|
69
|
-
ch_names: Iterable[str],
|
|
70
|
-
sfreq: int | float,
|
|
71
|
-
):
|
|
72
|
-
# no settings to test
|
|
73
|
-
pass
|
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
from numpy import array, cov
|
|
2
|
-
from typing import Iterable
|
|
3
|
-
|
|
4
|
-
from filterpy.kalman import KalmanFilter
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
def define_KF(Tp, sigma_w, sigma_v):
|
|
8
|
-
"""Define Kalman filter according to white noise acceleration model.
|
|
9
|
-
See DOI: 10.1109/TBME.2009.2038990 for explanation
|
|
10
|
-
See https://filterpy.readthedocs.io/en/latest/kalman/KalmanFilter.html#r64ca38088676-2 for implementation details
|
|
11
|
-
|
|
12
|
-
Parameters
|
|
13
|
-
----------
|
|
14
|
-
Tp : float
|
|
15
|
-
prediction interval
|
|
16
|
-
sigma_w : float
|
|
17
|
-
process noise
|
|
18
|
-
sigma_v : float
|
|
19
|
-
measurement noise
|
|
20
|
-
|
|
21
|
-
Returns
|
|
22
|
-
-------
|
|
23
|
-
filterpy.KalmanFilter
|
|
24
|
-
initialized KalmanFilter object
|
|
25
|
-
"""
|
|
26
|
-
|
|
27
|
-
f = KalmanFilter(dim_x=2, dim_z=1)
|
|
28
|
-
f.x = array([0, 1]) # x here sensor signal and it's first derivative
|
|
29
|
-
f.F = array([[1, Tp], [0, 1]])
|
|
30
|
-
f.H = array([[1, 0]])
|
|
31
|
-
f.R = sigma_v
|
|
32
|
-
f.Q = array([[(sigma_w**2)*(Tp**3)/3, (sigma_w**2)*(Tp**2)/2],
|
|
33
|
-
[(sigma_w**2)*(Tp**2)/2, (sigma_w**2)*Tp]])
|
|
34
|
-
f.P = cov([[1, 0], [0, 1]])
|
|
35
|
-
return f
|
|
36
|
-
|
|
37
|
-
def test_kf_settings(
|
|
38
|
-
s: dict,
|
|
39
|
-
ch_names: Iterable[str],
|
|
40
|
-
sfreq: int | float,
|
|
41
|
-
):
|
|
42
|
-
assert isinstance(s["kalman_filter_settings"]["Tp"], (float, int))
|
|
43
|
-
assert isinstance(s["kalman_filter_settings"]["sigma_w"], (float, int))
|
|
44
|
-
assert isinstance(s["kalman_filter_settings"]["sigma_v"], (float, int))
|
|
45
|
-
assert s["kalman_filter_settings"][
|
|
46
|
-
"frequency_bands"
|
|
47
|
-
], "No frequency bands specified for Kalman filter."
|
|
48
|
-
assert isinstance(
|
|
49
|
-
s["kalman_filter_settings"]["frequency_bands"], list
|
|
50
|
-
), "Frequency bands for Kalman filter must be specified as a list."
|
|
51
|
-
assert (
|
|
52
|
-
item
|
|
53
|
-
in s["frequency_ranges_hz"].values()
|
|
54
|
-
for item in s["kalman_filter_settings"]["frequency_bands"]
|
|
55
|
-
), (
|
|
56
|
-
"Frequency bands for Kalman filter must also be specified in "
|
|
57
|
-
"bandpass_filter_settings."
|
|
58
|
-
)
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
import numpy as np
|
|
2
|
-
from typing import Iterable
|
|
3
|
-
|
|
4
|
-
from py_neuromodulation import nm_features_abc
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
class LineLength(nm_features_abc.Feature):
|
|
8
|
-
def __init__(
|
|
9
|
-
self, settings: dict, ch_names: Iterable[str], sfreq: float
|
|
10
|
-
) -> None:
|
|
11
|
-
self.s = settings
|
|
12
|
-
self.ch_names = ch_names
|
|
13
|
-
|
|
14
|
-
@staticmethod
|
|
15
|
-
def get_line_length(x: np.ndarray) -> np.ndarray:
|
|
16
|
-
return np.mean(np.abs(np.diff(x)) / (x.shape[0] - 1))
|
|
17
|
-
|
|
18
|
-
@staticmethod
|
|
19
|
-
def test_settings(
|
|
20
|
-
settings: dict,
|
|
21
|
-
ch_names: Iterable[str],
|
|
22
|
-
sfreq: int | float,
|
|
23
|
-
):
|
|
24
|
-
# no settings to be checked
|
|
25
|
-
pass
|
|
26
|
-
|
|
27
|
-
def calc_feature(self, data: np.ndarray, features_compute: dict) -> dict:
|
|
28
|
-
for ch_idx, ch_name in enumerate(self.ch_names):
|
|
29
|
-
features_compute[
|
|
30
|
-
"_".join([ch_name, "LineLength"])
|
|
31
|
-
] = self.get_line_length(data[ch_idx, :])
|
|
32
|
-
|
|
33
|
-
return features_compute
|
|
@@ -1,112 +0,0 @@
|
|
|
1
|
-
from typing import Iterable
|
|
2
|
-
import numpy as np
|
|
3
|
-
|
|
4
|
-
import mne
|
|
5
|
-
from mne_connectivity import spectral_connectivity_epochs
|
|
6
|
-
|
|
7
|
-
from py_neuromodulation import nm_features_abc
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
class MNEConnectivity(nm_features_abc.Feature):
|
|
11
|
-
def __init__(
|
|
12
|
-
self,
|
|
13
|
-
settings: dict,
|
|
14
|
-
ch_names: Iterable[str],
|
|
15
|
-
sfreq: float,
|
|
16
|
-
) -> None:
|
|
17
|
-
self.s = settings
|
|
18
|
-
self.ch_names = ch_names
|
|
19
|
-
self.mode = settings["mne_connectiviy"]["mode"]
|
|
20
|
-
self.method = settings["mne_connectiviy"]["method"]
|
|
21
|
-
self.sfreq = sfreq
|
|
22
|
-
|
|
23
|
-
self.fbands = list(self.s["frequency_ranges_hz"].keys())
|
|
24
|
-
self.fband_ranges = []
|
|
25
|
-
|
|
26
|
-
@staticmethod
|
|
27
|
-
def test_settings(
|
|
28
|
-
settings: dict,
|
|
29
|
-
ch_names: Iterable[str],
|
|
30
|
-
sfreq: int | float,
|
|
31
|
-
):
|
|
32
|
-
# TODO: Double check passed parameters with mne_connectivity
|
|
33
|
-
pass
|
|
34
|
-
|
|
35
|
-
@staticmethod
|
|
36
|
-
def get_epoched_data(
|
|
37
|
-
raw: mne.io.RawArray, epoch_length: float = 1
|
|
38
|
-
) -> np.array:
|
|
39
|
-
time_samples_s = raw.get_data().shape[1] / raw.info["sfreq"]
|
|
40
|
-
if epoch_length > time_samples_s:
|
|
41
|
-
raise ValueError(
|
|
42
|
-
f"the intended epoch length for mne connectivity: {epoch_length}s"
|
|
43
|
-
f" are longer than the passed data array {np.round(time_samples_s, 2)}s"
|
|
44
|
-
)
|
|
45
|
-
events = mne.make_fixed_length_events(
|
|
46
|
-
raw, duration=epoch_length, overlap=0
|
|
47
|
-
)
|
|
48
|
-
event_id = {"rest": 1}
|
|
49
|
-
|
|
50
|
-
epochs = mne.Epochs(
|
|
51
|
-
raw,
|
|
52
|
-
events=events,
|
|
53
|
-
event_id=event_id,
|
|
54
|
-
tmin=0,
|
|
55
|
-
tmax=epoch_length,
|
|
56
|
-
baseline=None,
|
|
57
|
-
reject_by_annotation=True,
|
|
58
|
-
)
|
|
59
|
-
if epochs.events.shape[0] < 2:
|
|
60
|
-
raise Exception(
|
|
61
|
-
f"A minimum of 2 epochs is required for mne_connectivity,"
|
|
62
|
-
f" got only {epochs.events.shape[0]}. Increase settings['segment_length_features_ms']"
|
|
63
|
-
)
|
|
64
|
-
return epochs
|
|
65
|
-
|
|
66
|
-
def estimate_connectivity(self, epochs: mne.Epochs):
|
|
67
|
-
# n_jobs is here kept to 1, since setup of the multiprocessing Pool
|
|
68
|
-
# takes longer than most batch computing sizes
|
|
69
|
-
|
|
70
|
-
spec_out = spectral_connectivity_epochs(
|
|
71
|
-
data=epochs,
|
|
72
|
-
sfreq=self.sfreq,
|
|
73
|
-
n_jobs=1,
|
|
74
|
-
method=self.method,
|
|
75
|
-
mode=self.mode,
|
|
76
|
-
indices=(np.array([0, 0, 1, 1]), np.array([2, 3, 2, 3])),
|
|
77
|
-
faverage=False,
|
|
78
|
-
block_size=1000,
|
|
79
|
-
)
|
|
80
|
-
return spec_out
|
|
81
|
-
|
|
82
|
-
def calc_feature(self, data: np.array, features_compute: dict) -> dict:
|
|
83
|
-
|
|
84
|
-
raw = mne.io.RawArray(
|
|
85
|
-
data=data,
|
|
86
|
-
info=mne.create_info(ch_names=self.ch_names, sfreq=self.sfreq),
|
|
87
|
-
)
|
|
88
|
-
epochs = self.get_epoched_data(raw)
|
|
89
|
-
# there need to be minimum 2 of two epochs, otherwise mne_connectivity
|
|
90
|
-
# is not correctly initialized
|
|
91
|
-
|
|
92
|
-
spec_out = self.estimate_connectivity(epochs)
|
|
93
|
-
if len(self.fband_ranges) == 0:
|
|
94
|
-
for fband in self.fbands:
|
|
95
|
-
self.fband_ranges.append(
|
|
96
|
-
np.where(
|
|
97
|
-
np.logical_and(
|
|
98
|
-
np.array(spec_out.freqs)
|
|
99
|
-
> self.s["frequency_ranges_hz"][fband][0],
|
|
100
|
-
np.array(spec_out.freqs)
|
|
101
|
-
< self.s["frequency_ranges_hz"][fband][1],
|
|
102
|
-
)
|
|
103
|
-
)[0]
|
|
104
|
-
)
|
|
105
|
-
dat_conn = spec_out.get_data()
|
|
106
|
-
for conn in np.arange(dat_conn.shape[0]):
|
|
107
|
-
for fband_idx, fband in enumerate(self.fbands):
|
|
108
|
-
features_compute[
|
|
109
|
-
"_".join(["ch1", self.method, str(conn), fband])
|
|
110
|
-
] = np.mean(dat_conn[conn, self.fband_ranges[fband_idx]])
|
|
111
|
-
|
|
112
|
-
return features_compute
|
py_neuromodulation/nm_nolds.py
DELETED
|
@@ -1,93 +0,0 @@
|
|
|
1
|
-
import numpy as np
|
|
2
|
-
from typing import Iterable
|
|
3
|
-
import nolds
|
|
4
|
-
import warnings
|
|
5
|
-
|
|
6
|
-
from py_neuromodulation import nm_features_abc, nm_oscillatory
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
class Nolds(nm_features_abc.Feature):
|
|
10
|
-
def __init__(
|
|
11
|
-
self, settings: dict, ch_names: Iterable[str], sfreq: float
|
|
12
|
-
) -> None:
|
|
13
|
-
self.s = settings
|
|
14
|
-
self.ch_names = ch_names
|
|
15
|
-
|
|
16
|
-
if len(self.s["nolds_features"]["data"]["frequency_bands"]) > 0:
|
|
17
|
-
self.bp_filter = nm_oscillatory.BandPower(
|
|
18
|
-
settings, ch_names, sfreq, use_kf=False
|
|
19
|
-
)
|
|
20
|
-
|
|
21
|
-
@staticmethod
|
|
22
|
-
def test_settings(
|
|
23
|
-
s: dict,
|
|
24
|
-
ch_names: Iterable[str],
|
|
25
|
-
sfreq: int | float,
|
|
26
|
-
):
|
|
27
|
-
nolds_feature_cols = [
|
|
28
|
-
"sample_entropy",
|
|
29
|
-
"correlation_dimension",
|
|
30
|
-
"lyapunov_exponent",
|
|
31
|
-
"hurst_exponent",
|
|
32
|
-
"detrended_fluctutaion_analysis",
|
|
33
|
-
]
|
|
34
|
-
if sum([s["nolds_features"][f] for f in nolds_feature_cols]) == 0:
|
|
35
|
-
warnings.warn(
|
|
36
|
-
"nolds feature enabled, but no nolds_feature type selected"
|
|
37
|
-
)
|
|
38
|
-
|
|
39
|
-
for fb in s["nolds_features"]["data"]["frequency_bands"]:
|
|
40
|
-
assert fb in list(
|
|
41
|
-
s["frequency_ranges_hz"].keys()
|
|
42
|
-
), f"{fb} selected in nolds_features, but not defined in s['frequency_ranges_hz']"
|
|
43
|
-
|
|
44
|
-
def calc_feature(
|
|
45
|
-
self,
|
|
46
|
-
data: np.array,
|
|
47
|
-
features_compute: dict,
|
|
48
|
-
) -> dict:
|
|
49
|
-
|
|
50
|
-
data = np.nan_to_num(data)
|
|
51
|
-
if self.s["nolds_features"]["data"]["raw"]:
|
|
52
|
-
features_compute = self.calc_nolds(data, features_compute)
|
|
53
|
-
if len(self.s["nolds_features"]["data"]["frequency_bands"]) > 0:
|
|
54
|
-
data_filt = self.bp_filter.bandpass_filter.filter_data(data)
|
|
55
|
-
|
|
56
|
-
for f_band_idx, f_band in enumerate(
|
|
57
|
-
self.s["nolds_features"]["data"]["frequency_bands"]
|
|
58
|
-
):
|
|
59
|
-
# filter data now for a specific fband and pass to calc_nolds
|
|
60
|
-
features_compute = self.calc_nolds(
|
|
61
|
-
data_filt[:, f_band_idx, :], features_compute, f_band
|
|
62
|
-
) # ch, bands, samples
|
|
63
|
-
return features_compute
|
|
64
|
-
|
|
65
|
-
def calc_nolds(
|
|
66
|
-
self, data: np.array, features_compute: dict, data_str: str = "raw"
|
|
67
|
-
) -> dict:
|
|
68
|
-
|
|
69
|
-
for ch_idx, ch_name in enumerate(self.ch_names):
|
|
70
|
-
dat = data[ch_idx, :]
|
|
71
|
-
empty_arr = dat.sum() == 0
|
|
72
|
-
if self.s["nolds_features"]["sample_entropy"]:
|
|
73
|
-
features_compute[
|
|
74
|
-
f"{ch_name}_nolds_sample_entropy"
|
|
75
|
-
] = nolds.sampen(dat) if not empty_arr else 0
|
|
76
|
-
if self.s["nolds_features"]["correlation_dimension"]:
|
|
77
|
-
features_compute[
|
|
78
|
-
f"{ch_name}_nolds_correlation_dimension_{data_str}"
|
|
79
|
-
] = nolds.corr_dim(dat, emb_dim=2) if not empty_arr else 0
|
|
80
|
-
if self.s["nolds_features"]["lyapunov_exponent"]:
|
|
81
|
-
features_compute[
|
|
82
|
-
f"{ch_name}_nolds_lyapunov_exponent_{data_str}"
|
|
83
|
-
] = nolds.lyap_r(dat) if not empty_arr else 0
|
|
84
|
-
if self.s["nolds_features"]["hurst_exponent"]:
|
|
85
|
-
features_compute[
|
|
86
|
-
f"{ch_name}_nolds_hurst_exponent_{data_str}"
|
|
87
|
-
] = nolds.hurst_rs(dat) if not empty_arr else 0
|
|
88
|
-
if self.s["nolds_features"]["detrended_fluctutaion_analysis"]:
|
|
89
|
-
features_compute[
|
|
90
|
-
f"{ch_name}_nolds_detrended_fluctutaion_analysis_{data_str}"
|
|
91
|
-
] = nolds.dfa(dat) if not empty_arr else 0
|
|
92
|
-
|
|
93
|
-
return features_compute
|
|
@@ -1,214 +0,0 @@
|
|
|
1
|
-
"""Module for real-time data normalization."""
|
|
2
|
-
from enum import Enum
|
|
3
|
-
|
|
4
|
-
from sklearn import preprocessing
|
|
5
|
-
import numpy as np
|
|
6
|
-
class NORM_METHODS(Enum):
|
|
7
|
-
MEAN = "mean"
|
|
8
|
-
MEDIAN = "median"
|
|
9
|
-
ZSCORE = "zscore"
|
|
10
|
-
ZSCORE_MEDIAN = "zscore-median"
|
|
11
|
-
QUANTILE = "quantile"
|
|
12
|
-
POWER = "power"
|
|
13
|
-
ROBUST = "robust"
|
|
14
|
-
MINMAX = "minmax"
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
def test_normalization_settings(
|
|
18
|
-
normalization_time_s: int | float, normalization_method: str, clip: bool
|
|
19
|
-
):
|
|
20
|
-
assert isinstance(
|
|
21
|
-
normalization_time_s,
|
|
22
|
-
(float, int),
|
|
23
|
-
)
|
|
24
|
-
|
|
25
|
-
assert isinstance(
|
|
26
|
-
normalization_method, str
|
|
27
|
-
), "normalization method needs to be of type string"
|
|
28
|
-
|
|
29
|
-
assert normalization_method in [e.value for e in NORM_METHODS], (
|
|
30
|
-
f"select a valid normalization method, got {normalization_method}, "
|
|
31
|
-
f"valid options are {[e.value for e in NORM_METHODS]}"
|
|
32
|
-
)
|
|
33
|
-
|
|
34
|
-
assert isinstance(clip, (float, int, bool))
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
class RawNormalizer:
|
|
38
|
-
def __init__(
|
|
39
|
-
self,
|
|
40
|
-
sfreq: int | float,
|
|
41
|
-
sampling_rate_features_hz: int,
|
|
42
|
-
normalization_method: str = "zscore",
|
|
43
|
-
normalization_time_s: int | float = 30,
|
|
44
|
-
clip: bool | int | float = False,
|
|
45
|
-
) -> None:
|
|
46
|
-
"""Normalize raw data.
|
|
47
|
-
|
|
48
|
-
normalize_samples : int
|
|
49
|
-
number of past samples considered for normalization
|
|
50
|
-
sample_add : int
|
|
51
|
-
number of samples to add to previous
|
|
52
|
-
method : str | default is 'mean'
|
|
53
|
-
data is normalized via subtraction of the 'mean' or 'median' and
|
|
54
|
-
subsequent division by the 'mean' or 'median'. For z-scoring enter
|
|
55
|
-
'zscore'.
|
|
56
|
-
clip : int | float, optional
|
|
57
|
-
value at which to clip after normalization
|
|
58
|
-
"""
|
|
59
|
-
|
|
60
|
-
test_normalization_settings(normalization_time_s, normalization_method, clip)
|
|
61
|
-
|
|
62
|
-
self.method = normalization_method
|
|
63
|
-
self.clip = clip
|
|
64
|
-
self.num_samples_normalize = int(normalization_time_s * sfreq)
|
|
65
|
-
self.add_samples = int(sfreq / sampling_rate_features_hz)
|
|
66
|
-
self.previous = None
|
|
67
|
-
|
|
68
|
-
def process(self, data: np.ndarray) -> np.ndarray:
|
|
69
|
-
data = data.T
|
|
70
|
-
if self.previous is None:
|
|
71
|
-
self.previous = data
|
|
72
|
-
return data.T
|
|
73
|
-
|
|
74
|
-
self.previous = np.vstack((self.previous, data[-self.add_samples :]))
|
|
75
|
-
|
|
76
|
-
data, self.previous = _normalize_and_clip(
|
|
77
|
-
current=data,
|
|
78
|
-
previous=self.previous,
|
|
79
|
-
method=self.method,
|
|
80
|
-
clip=self.clip,
|
|
81
|
-
description="raw",
|
|
82
|
-
)
|
|
83
|
-
if self.previous.shape[0] >= self.num_samples_normalize:
|
|
84
|
-
self.previous = self.previous[1:]
|
|
85
|
-
|
|
86
|
-
return data.T
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
class FeatureNormalizer:
|
|
90
|
-
def __init__(
|
|
91
|
-
self,
|
|
92
|
-
sampling_rate_features_hz: int,
|
|
93
|
-
normalization_method: str = "zscore",
|
|
94
|
-
normalization_time_s: int | float = 30,
|
|
95
|
-
clip: bool | int | float = False,
|
|
96
|
-
) -> None:
|
|
97
|
-
"""Normalize raw data.
|
|
98
|
-
|
|
99
|
-
normalize_samples : int
|
|
100
|
-
number of past samples considered for normalization
|
|
101
|
-
sample_add : int
|
|
102
|
-
number of samples to add to previous
|
|
103
|
-
method : str | default is 'mean'
|
|
104
|
-
data is normalized via subtraction of the 'mean' or 'median' and
|
|
105
|
-
subsequent division by the 'mean' or 'median'. For z-scoring enter
|
|
106
|
-
'zscore'.
|
|
107
|
-
clip : int | float, optional
|
|
108
|
-
value at which to clip after normalization
|
|
109
|
-
"""
|
|
110
|
-
|
|
111
|
-
test_normalization_settings(normalization_time_s, normalization_method, clip)
|
|
112
|
-
|
|
113
|
-
self.method = normalization_method
|
|
114
|
-
self.clip = clip
|
|
115
|
-
self.num_samples_normalize = int(
|
|
116
|
-
normalization_time_s * sampling_rate_features_hz
|
|
117
|
-
)
|
|
118
|
-
self.previous = None
|
|
119
|
-
|
|
120
|
-
def process(self, data: np.ndarray) -> np.ndarray:
|
|
121
|
-
if self.previous is None:
|
|
122
|
-
self.previous = data
|
|
123
|
-
return data
|
|
124
|
-
|
|
125
|
-
self.previous = np.vstack((self.previous, data))
|
|
126
|
-
|
|
127
|
-
data, self.previous = _normalize_and_clip(
|
|
128
|
-
current=data,
|
|
129
|
-
previous=self.previous,
|
|
130
|
-
method=self.method,
|
|
131
|
-
clip=self.clip,
|
|
132
|
-
description="feature",
|
|
133
|
-
)
|
|
134
|
-
if self.previous.shape[0] >= self.num_samples_normalize:
|
|
135
|
-
self.previous = self.previous[1:]
|
|
136
|
-
|
|
137
|
-
return data
|
|
138
|
-
|
|
139
|
-
"""
|
|
140
|
-
Functions to check for NaN's before deciding which Numpy function to call
|
|
141
|
-
"""
|
|
142
|
-
def nan_mean(data, axis):
|
|
143
|
-
return np.nanmean(data, axis=axis) if np.any(np.isnan(sum(data))) else np.mean(data, axis=axis)
|
|
144
|
-
|
|
145
|
-
def nan_std(data, axis):
|
|
146
|
-
return np.nanstd(data, axis=axis) if np.any(np.isnan(sum(data))) else np.std(data, axis=axis)
|
|
147
|
-
|
|
148
|
-
def nan_median(data, axis):
|
|
149
|
-
return np.nanmedian(data, axis=axis) if np.any(np.isnan(sum(data))) else np.median(data, axis=axis)
|
|
150
|
-
|
|
151
|
-
def _normalize_and_clip(
|
|
152
|
-
current: np.ndarray,
|
|
153
|
-
previous: np.ndarray,
|
|
154
|
-
method: str,
|
|
155
|
-
clip: int | float | bool,
|
|
156
|
-
description: str,
|
|
157
|
-
) -> tuple[np.ndarray, np.ndarray]:
|
|
158
|
-
"""Normalize data."""
|
|
159
|
-
match method:
|
|
160
|
-
case NORM_METHODS.MEAN.value:
|
|
161
|
-
mean = nan_mean(previous, axis=0)
|
|
162
|
-
current = (current - mean) / mean
|
|
163
|
-
case NORM_METHODS.MEDIAN.value:
|
|
164
|
-
median = nan_median(previous, axis=0)
|
|
165
|
-
current = (current - median) / median
|
|
166
|
-
case NORM_METHODS.ZSCORE.value:
|
|
167
|
-
current = (current - nan_mean(previous, axis=0)) / nan_std(previous, axis=0)
|
|
168
|
-
case NORM_METHODS.ZSCORE_MEDIAN.value:
|
|
169
|
-
current = (current - nan_median(previous, axis=0)) / nan_std(previous, axis=0)
|
|
170
|
-
# For the following methods we check for the shape of current
|
|
171
|
-
# when current is a 1D array, then it is the post-processing normalization,
|
|
172
|
-
# and we need to expand, and remove the extra dimension afterwards
|
|
173
|
-
# When current is a 2D array, then it is pre-processing normalization, and
|
|
174
|
-
# there's no need for expanding.
|
|
175
|
-
case (NORM_METHODS.QUANTILE.value |
|
|
176
|
-
NORM_METHODS.ROBUST.value |
|
|
177
|
-
NORM_METHODS.MINMAX.value |
|
|
178
|
-
NORM_METHODS.POWER.value):
|
|
179
|
-
|
|
180
|
-
norm_methods = {
|
|
181
|
-
NORM_METHODS.QUANTILE.value : lambda: preprocessing.QuantileTransformer(n_quantiles=300),
|
|
182
|
-
NORM_METHODS.ROBUST.value : preprocessing.RobustScaler,
|
|
183
|
-
NORM_METHODS.MINMAX.value : preprocessing.MinMaxScaler,
|
|
184
|
-
NORM_METHODS.POWER.value : preprocessing.PowerTransformer
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
current = (
|
|
188
|
-
norm_methods[method]()
|
|
189
|
-
.fit(np.nan_to_num(previous))
|
|
190
|
-
.transform(
|
|
191
|
-
# if post-processing: pad dimensions to 2
|
|
192
|
-
np.reshape(current, (2-len(current.shape))*(1,) + current.shape)
|
|
193
|
-
)
|
|
194
|
-
.squeeze() # if post-processing: remove extra dimension
|
|
195
|
-
)
|
|
196
|
-
|
|
197
|
-
case _:
|
|
198
|
-
raise ValueError(
|
|
199
|
-
f"Only {[e.value for e in NORM_METHODS]} are supported as "
|
|
200
|
-
f"{description} normalization methods. Got {method}."
|
|
201
|
-
)
|
|
202
|
-
|
|
203
|
-
if clip:
|
|
204
|
-
current = _clip(data=current, clip=clip)
|
|
205
|
-
return current, previous
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
def _clip(data: np.ndarray, clip: bool | int | float) -> np.ndarray:
|
|
209
|
-
"""Clip data."""
|
|
210
|
-
if clip is True:
|
|
211
|
-
clip = 3.0 # default value
|
|
212
|
-
else:
|
|
213
|
-
clip = float(clip)
|
|
214
|
-
return np.nan_to_num(data).clip(min=-clip, max=clip)
|