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.
Files changed (109) hide show
  1. py_neuromodulation/ConnectivityDecoding/_get_grid_hull.m +34 -34
  2. py_neuromodulation/ConnectivityDecoding/_get_grid_whole_brain.py +95 -106
  3. py_neuromodulation/ConnectivityDecoding/_helper_write_connectome.py +107 -119
  4. py_neuromodulation/__init__.py +80 -13
  5. py_neuromodulation/{nm_RMAP.py → analysis/RMAP.py} +496 -531
  6. py_neuromodulation/analysis/__init__.py +4 -0
  7. py_neuromodulation/{nm_decode.py → analysis/decode.py} +918 -992
  8. py_neuromodulation/{nm_analysis.py → analysis/feature_reader.py} +994 -1074
  9. py_neuromodulation/{nm_plots.py → analysis/plots.py} +627 -612
  10. py_neuromodulation/{nm_stats.py → analysis/stats.py} +458 -480
  11. py_neuromodulation/data/README +6 -6
  12. py_neuromodulation/data/dataset_description.json +8 -8
  13. py_neuromodulation/data/participants.json +32 -32
  14. py_neuromodulation/data/participants.tsv +2 -2
  15. py_neuromodulation/data/sub-testsub/ses-EphysMedOff/ieeg/sub-testsub_ses-EphysMedOff_space-mni_coordsystem.json +5 -5
  16. py_neuromodulation/data/sub-testsub/ses-EphysMedOff/ieeg/sub-testsub_ses-EphysMedOff_space-mni_electrodes.tsv +11 -11
  17. py_neuromodulation/data/sub-testsub/ses-EphysMedOff/ieeg/sub-testsub_ses-EphysMedOff_task-gripforce_run-0_channels.tsv +11 -11
  18. py_neuromodulation/data/sub-testsub/ses-EphysMedOff/ieeg/sub-testsub_ses-EphysMedOff_task-gripforce_run-0_ieeg.json +18 -18
  19. py_neuromodulation/data/sub-testsub/ses-EphysMedOff/ieeg/sub-testsub_ses-EphysMedOff_task-gripforce_run-0_ieeg.vhdr +35 -35
  20. py_neuromodulation/data/sub-testsub/ses-EphysMedOff/ieeg/sub-testsub_ses-EphysMedOff_task-gripforce_run-0_ieeg.vmrk +13 -13
  21. py_neuromodulation/data/sub-testsub/ses-EphysMedOff/sub-testsub_ses-EphysMedOff_scans.tsv +2 -2
  22. py_neuromodulation/default_settings.yaml +241 -0
  23. py_neuromodulation/features/__init__.py +31 -0
  24. py_neuromodulation/features/bandpower.py +165 -0
  25. py_neuromodulation/features/bispectra.py +157 -0
  26. py_neuromodulation/features/bursts.py +297 -0
  27. py_neuromodulation/features/coherence.py +255 -0
  28. py_neuromodulation/features/feature_processor.py +121 -0
  29. py_neuromodulation/features/fooof.py +142 -0
  30. py_neuromodulation/features/hjorth_raw.py +57 -0
  31. py_neuromodulation/features/linelength.py +21 -0
  32. py_neuromodulation/features/mne_connectivity.py +148 -0
  33. py_neuromodulation/features/nolds.py +94 -0
  34. py_neuromodulation/features/oscillatory.py +249 -0
  35. py_neuromodulation/features/sharpwaves.py +432 -0
  36. py_neuromodulation/filter/__init__.py +3 -0
  37. py_neuromodulation/filter/kalman_filter.py +67 -0
  38. py_neuromodulation/filter/kalman_filter_external.py +1890 -0
  39. py_neuromodulation/filter/mne_filter.py +128 -0
  40. py_neuromodulation/filter/notch_filter.py +93 -0
  41. py_neuromodulation/grid_cortex.tsv +40 -40
  42. py_neuromodulation/liblsl/libpugixml.so.1.12 +0 -0
  43. py_neuromodulation/liblsl/linux/bionic_amd64/liblsl.1.16.2.so +0 -0
  44. py_neuromodulation/liblsl/linux/bookworm_amd64/liblsl.1.16.2.so +0 -0
  45. py_neuromodulation/liblsl/linux/focal_amd46/liblsl.1.16.2.so +0 -0
  46. py_neuromodulation/liblsl/linux/jammy_amd64/liblsl.1.16.2.so +0 -0
  47. py_neuromodulation/liblsl/linux/jammy_x86/liblsl.1.16.2.so +0 -0
  48. py_neuromodulation/liblsl/linux/noble_amd64/liblsl.1.16.2.so +0 -0
  49. py_neuromodulation/liblsl/macos/amd64/liblsl.1.16.2.dylib +0 -0
  50. py_neuromodulation/liblsl/macos/arm64/liblsl.1.16.0.dylib +0 -0
  51. py_neuromodulation/liblsl/windows/amd64/liblsl.1.16.2.dll +0 -0
  52. py_neuromodulation/liblsl/windows/x86/liblsl.1.16.2.dll +0 -0
  53. py_neuromodulation/processing/__init__.py +10 -0
  54. py_neuromodulation/{nm_artifacts.py → processing/artifacts.py} +29 -25
  55. py_neuromodulation/processing/data_preprocessor.py +77 -0
  56. py_neuromodulation/processing/filter_preprocessing.py +78 -0
  57. py_neuromodulation/processing/normalization.py +175 -0
  58. py_neuromodulation/{nm_projection.py → processing/projection.py} +370 -394
  59. py_neuromodulation/{nm_rereference.py → processing/rereference.py} +97 -95
  60. py_neuromodulation/{nm_resample.py → processing/resample.py} +56 -50
  61. py_neuromodulation/stream/__init__.py +3 -0
  62. py_neuromodulation/stream/data_processor.py +325 -0
  63. py_neuromodulation/stream/generator.py +53 -0
  64. py_neuromodulation/stream/mnelsl_player.py +94 -0
  65. py_neuromodulation/stream/mnelsl_stream.py +120 -0
  66. py_neuromodulation/stream/settings.py +292 -0
  67. py_neuromodulation/stream/stream.py +427 -0
  68. py_neuromodulation/utils/__init__.py +2 -0
  69. py_neuromodulation/{nm_define_nmchannels.py → utils/channels.py} +305 -302
  70. py_neuromodulation/utils/database.py +149 -0
  71. py_neuromodulation/utils/io.py +378 -0
  72. py_neuromodulation/utils/keyboard.py +52 -0
  73. py_neuromodulation/utils/logging.py +66 -0
  74. py_neuromodulation/utils/types.py +251 -0
  75. {py_neuromodulation-0.0.4.dist-info → py_neuromodulation-0.0.6.dist-info}/METADATA +28 -33
  76. py_neuromodulation-0.0.6.dist-info/RECORD +89 -0
  77. {py_neuromodulation-0.0.4.dist-info → py_neuromodulation-0.0.6.dist-info}/WHEEL +1 -1
  78. {py_neuromodulation-0.0.4.dist-info → py_neuromodulation-0.0.6.dist-info}/licenses/LICENSE +21 -21
  79. py_neuromodulation/FieldTrip.py +0 -589
  80. py_neuromodulation/_write_example_dataset_helper.py +0 -65
  81. py_neuromodulation/nm_EpochStream.py +0 -92
  82. py_neuromodulation/nm_IO.py +0 -417
  83. py_neuromodulation/nm_across_patient_decoding.py +0 -927
  84. py_neuromodulation/nm_bispectra.py +0 -168
  85. py_neuromodulation/nm_bursts.py +0 -198
  86. py_neuromodulation/nm_coherence.py +0 -205
  87. py_neuromodulation/nm_cohortwrapper.py +0 -435
  88. py_neuromodulation/nm_eval_timing.py +0 -239
  89. py_neuromodulation/nm_features.py +0 -116
  90. py_neuromodulation/nm_features_abc.py +0 -39
  91. py_neuromodulation/nm_filter.py +0 -219
  92. py_neuromodulation/nm_filter_preprocessing.py +0 -91
  93. py_neuromodulation/nm_fooof.py +0 -159
  94. py_neuromodulation/nm_generator.py +0 -37
  95. py_neuromodulation/nm_hjorth_raw.py +0 -73
  96. py_neuromodulation/nm_kalmanfilter.py +0 -58
  97. py_neuromodulation/nm_linelength.py +0 -33
  98. py_neuromodulation/nm_mne_connectivity.py +0 -112
  99. py_neuromodulation/nm_nolds.py +0 -93
  100. py_neuromodulation/nm_normalization.py +0 -214
  101. py_neuromodulation/nm_oscillatory.py +0 -448
  102. py_neuromodulation/nm_run_analysis.py +0 -435
  103. py_neuromodulation/nm_settings.json +0 -338
  104. py_neuromodulation/nm_settings.py +0 -68
  105. py_neuromodulation/nm_sharpwaves.py +0 -401
  106. py_neuromodulation/nm_stream_abc.py +0 -218
  107. py_neuromodulation/nm_stream_offline.py +0 -359
  108. py_neuromodulation/utils/_logging.py +0 -24
  109. 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
@@ -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)