eegdash 0.3.9.dev170082126__py3-none-any.whl → 0.4.0__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.
Potentially problematic release.
This version of eegdash might be problematic. Click here for more details.
- eegdash/__init__.py +12 -1
- eegdash/api.py +297 -295
- eegdash/bids_eeg_metadata.py +297 -56
- eegdash/const.py +43 -0
- eegdash/data_utils.py +327 -430
- eegdash/dataset/__init__.py +19 -1
- eegdash/dataset/dataset.py +61 -33
- eegdash/dataset/dataset_summary.csv +255 -256
- eegdash/dataset/registry.py +163 -11
- eegdash/downloader.py +197 -0
- eegdash/features/datasets.py +323 -138
- eegdash/features/decorators.py +88 -3
- eegdash/features/extractors.py +203 -55
- eegdash/features/feature_bank/complexity.py +7 -3
- eegdash/features/feature_bank/dimensionality.py +1 -1
- eegdash/features/inspect.py +80 -5
- eegdash/features/serialization.py +49 -17
- eegdash/features/utils.py +75 -8
- eegdash/hbn/__init__.py +11 -0
- eegdash/hbn/preprocessing.py +61 -19
- eegdash/hbn/windows.py +157 -34
- eegdash/logging.py +54 -0
- eegdash/mongodb.py +55 -24
- eegdash/paths.py +28 -5
- eegdash/utils.py +29 -1
- {eegdash-0.3.9.dev170082126.dist-info → eegdash-0.4.0.dist-info}/METADATA +11 -59
- eegdash-0.4.0.dist-info/RECORD +37 -0
- eegdash-0.3.9.dev170082126.dist-info/RECORD +0 -35
- {eegdash-0.3.9.dev170082126.dist-info → eegdash-0.4.0.dist-info}/WHEEL +0 -0
- {eegdash-0.3.9.dev170082126.dist-info → eegdash-0.4.0.dist-info}/licenses/LICENSE +0 -0
- {eegdash-0.3.9.dev170082126.dist-info → eegdash-0.4.0.dist-info}/top_level.txt +0 -0
eegdash/features/decorators.py
CHANGED
|
@@ -12,6 +12,21 @@ from .extractors import (
|
|
|
12
12
|
|
|
13
13
|
|
|
14
14
|
class FeaturePredecessor:
|
|
15
|
+
"""A decorator to specify parent extractors for a feature function.
|
|
16
|
+
|
|
17
|
+
This decorator attaches a list of parent extractor types to a feature
|
|
18
|
+
extraction function. This information can be used to build a dependency
|
|
19
|
+
graph of features.
|
|
20
|
+
|
|
21
|
+
Parameters
|
|
22
|
+
----------
|
|
23
|
+
*parent_extractor_type : list of Type
|
|
24
|
+
A list of feature extractor classes (subclasses of
|
|
25
|
+
:class:`~eegdash.features.extractors.FeatureExtractor`) that this
|
|
26
|
+
feature depends on.
|
|
27
|
+
|
|
28
|
+
"""
|
|
29
|
+
|
|
15
30
|
def __init__(self, *parent_extractor_type: List[Type]):
|
|
16
31
|
parent_cls = parent_extractor_type
|
|
17
32
|
if not parent_cls:
|
|
@@ -20,17 +35,58 @@ class FeaturePredecessor:
|
|
|
20
35
|
assert issubclass(p_cls, FeatureExtractor)
|
|
21
36
|
self.parent_extractor_type = parent_cls
|
|
22
37
|
|
|
23
|
-
def __call__(self, func: Callable):
|
|
38
|
+
def __call__(self, func: Callable) -> Callable:
|
|
39
|
+
"""Apply the decorator to a function.
|
|
40
|
+
|
|
41
|
+
Parameters
|
|
42
|
+
----------
|
|
43
|
+
func : callable
|
|
44
|
+
The feature extraction function to decorate.
|
|
45
|
+
|
|
46
|
+
Returns
|
|
47
|
+
-------
|
|
48
|
+
callable
|
|
49
|
+
The decorated function with the `parent_extractor_type` attribute
|
|
50
|
+
set.
|
|
51
|
+
|
|
52
|
+
"""
|
|
24
53
|
f = _get_underlying_func(func)
|
|
25
54
|
f.parent_extractor_type = self.parent_extractor_type
|
|
26
55
|
return func
|
|
27
56
|
|
|
28
57
|
|
|
29
58
|
class FeatureKind:
|
|
59
|
+
"""A decorator to specify the kind of a feature.
|
|
60
|
+
|
|
61
|
+
This decorator attaches a "feature kind" (e.g., univariate, bivariate)
|
|
62
|
+
to a feature extraction function.
|
|
63
|
+
|
|
64
|
+
Parameters
|
|
65
|
+
----------
|
|
66
|
+
feature_kind : MultivariateFeature
|
|
67
|
+
An instance of a feature kind class, such as
|
|
68
|
+
:class:`~eegdash.features.extractors.UnivariateFeature` or
|
|
69
|
+
:class:`~eegdash.features.extractors.BivariateFeature`.
|
|
70
|
+
|
|
71
|
+
"""
|
|
72
|
+
|
|
30
73
|
def __init__(self, feature_kind: MultivariateFeature):
|
|
31
74
|
self.feature_kind = feature_kind
|
|
32
75
|
|
|
33
|
-
def __call__(self, func):
|
|
76
|
+
def __call__(self, func: Callable) -> Callable:
|
|
77
|
+
"""Apply the decorator to a function.
|
|
78
|
+
|
|
79
|
+
Parameters
|
|
80
|
+
----------
|
|
81
|
+
func : callable
|
|
82
|
+
The feature extraction function to decorate.
|
|
83
|
+
|
|
84
|
+
Returns
|
|
85
|
+
-------
|
|
86
|
+
callable
|
|
87
|
+
The decorated function with the `feature_kind` attribute set.
|
|
88
|
+
|
|
89
|
+
"""
|
|
34
90
|
f = _get_underlying_func(func)
|
|
35
91
|
f.feature_kind = self.feature_kind
|
|
36
92
|
return func
|
|
@@ -38,9 +94,33 @@ class FeatureKind:
|
|
|
38
94
|
|
|
39
95
|
# Syntax sugar
|
|
40
96
|
univariate_feature = FeatureKind(UnivariateFeature())
|
|
97
|
+
"""Decorator to mark a feature as univariate.
|
|
98
|
+
|
|
99
|
+
This is a convenience instance of :class:`FeatureKind` pre-configured for
|
|
100
|
+
univariate features.
|
|
101
|
+
"""
|
|
41
102
|
|
|
42
103
|
|
|
43
|
-
def bivariate_feature(func, directed=False):
|
|
104
|
+
def bivariate_feature(func: Callable, directed: bool = False) -> Callable:
|
|
105
|
+
"""Decorator to mark a feature as bivariate.
|
|
106
|
+
|
|
107
|
+
This decorator specifies that the feature operates on pairs of channels.
|
|
108
|
+
|
|
109
|
+
Parameters
|
|
110
|
+
----------
|
|
111
|
+
func : callable
|
|
112
|
+
The feature extraction function to decorate.
|
|
113
|
+
directed : bool, default False
|
|
114
|
+
If True, the feature is directed (e.g., connectivity from channel A
|
|
115
|
+
to B is different from B to A). If False, the feature is undirected.
|
|
116
|
+
|
|
117
|
+
Returns
|
|
118
|
+
-------
|
|
119
|
+
callable
|
|
120
|
+
The decorated function with the appropriate bivariate feature kind
|
|
121
|
+
attached.
|
|
122
|
+
|
|
123
|
+
"""
|
|
44
124
|
if directed:
|
|
45
125
|
kind = DirectedBivariateFeature()
|
|
46
126
|
else:
|
|
@@ -49,3 +129,8 @@ def bivariate_feature(func, directed=False):
|
|
|
49
129
|
|
|
50
130
|
|
|
51
131
|
multivariate_feature = FeatureKind(MultivariateFeature())
|
|
132
|
+
"""Decorator to mark a feature as multivariate.
|
|
133
|
+
|
|
134
|
+
This is a convenience instance of :class:`FeatureKind` pre-configured for
|
|
135
|
+
multivariate features, which operate on all channels simultaneously.
|
|
136
|
+
"""
|
eegdash/features/extractors.py
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
from abc import ABC, abstractmethod
|
|
2
4
|
from collections.abc import Callable
|
|
3
5
|
from functools import partial
|
|
@@ -7,7 +9,23 @@ import numpy as np
|
|
|
7
9
|
from numba.core.dispatcher import Dispatcher
|
|
8
10
|
|
|
9
11
|
|
|
10
|
-
def _get_underlying_func(func):
|
|
12
|
+
def _get_underlying_func(func: Callable) -> Callable:
|
|
13
|
+
"""Get the underlying function from a potential wrapper.
|
|
14
|
+
|
|
15
|
+
This helper unwraps functions that might be wrapped by `functools.partial`
|
|
16
|
+
or `numba.dispatcher.Dispatcher`.
|
|
17
|
+
|
|
18
|
+
Parameters
|
|
19
|
+
----------
|
|
20
|
+
func : callable
|
|
21
|
+
The function to unwrap.
|
|
22
|
+
|
|
23
|
+
Returns
|
|
24
|
+
-------
|
|
25
|
+
callable
|
|
26
|
+
The underlying Python function.
|
|
27
|
+
|
|
28
|
+
"""
|
|
11
29
|
f = func
|
|
12
30
|
if isinstance(f, partial):
|
|
13
31
|
f = f.func
|
|
@@ -17,22 +35,46 @@ def _get_underlying_func(func):
|
|
|
17
35
|
|
|
18
36
|
|
|
19
37
|
class TrainableFeature(ABC):
|
|
38
|
+
"""Abstract base class for features that require training.
|
|
39
|
+
|
|
40
|
+
This ABC defines the interface for feature extractors that need to be
|
|
41
|
+
fitted on data before they can be used. It includes methods for fitting
|
|
42
|
+
the feature extractor and for resetting its state.
|
|
43
|
+
"""
|
|
44
|
+
|
|
20
45
|
def __init__(self):
|
|
21
46
|
self._is_trained = False
|
|
22
47
|
self.clear()
|
|
23
48
|
|
|
24
49
|
@abstractmethod
|
|
25
50
|
def clear(self):
|
|
51
|
+
"""Reset the internal state of the feature extractor."""
|
|
26
52
|
pass
|
|
27
53
|
|
|
28
54
|
@abstractmethod
|
|
29
55
|
def partial_fit(self, *x, y=None):
|
|
56
|
+
"""Update the feature extractor's state with a batch of data.
|
|
57
|
+
|
|
58
|
+
Parameters
|
|
59
|
+
----------
|
|
60
|
+
*x : tuple
|
|
61
|
+
The input data for fitting.
|
|
62
|
+
y : any, optional
|
|
63
|
+
The target data, if required for supervised training.
|
|
64
|
+
|
|
65
|
+
"""
|
|
30
66
|
pass
|
|
31
67
|
|
|
32
68
|
def fit(self):
|
|
69
|
+
"""Finalize the training of the feature extractor.
|
|
70
|
+
|
|
71
|
+
This method should be called after all data has been seen via
|
|
72
|
+
`partial_fit`. It marks the feature as fitted.
|
|
73
|
+
"""
|
|
33
74
|
self._is_fitted = True
|
|
34
75
|
|
|
35
76
|
def __call__(self, *args, **kwargs):
|
|
77
|
+
"""Check if the feature is fitted before execution."""
|
|
36
78
|
if not self._is_fitted:
|
|
37
79
|
raise RuntimeError(
|
|
38
80
|
f"{self.__class__} cannot be called, it has to be fitted first."
|
|
@@ -40,6 +82,22 @@ class TrainableFeature(ABC):
|
|
|
40
82
|
|
|
41
83
|
|
|
42
84
|
class FeatureExtractor(TrainableFeature):
|
|
85
|
+
"""A composite feature extractor that applies multiple feature functions.
|
|
86
|
+
|
|
87
|
+
This class orchestrates the application of a dictionary of feature
|
|
88
|
+
extraction functions to input data. It can handle nested extractors,
|
|
89
|
+
pre-processing, and trainable features.
|
|
90
|
+
|
|
91
|
+
Parameters
|
|
92
|
+
----------
|
|
93
|
+
feature_extractors : dict[str, callable]
|
|
94
|
+
A dictionary where keys are feature names and values are the feature
|
|
95
|
+
extraction functions or other `FeatureExtractor` instances.
|
|
96
|
+
**preprocess_kwargs
|
|
97
|
+
Keyword arguments to be passed to the `preprocess` method.
|
|
98
|
+
|
|
99
|
+
"""
|
|
100
|
+
|
|
43
101
|
def __init__(
|
|
44
102
|
self, feature_extractors: Dict[str, Callable], **preprocess_kwargs: Dict
|
|
45
103
|
):
|
|
@@ -63,30 +121,64 @@ class FeatureExtractor(TrainableFeature):
|
|
|
63
121
|
if isinstance(fe, partial):
|
|
64
122
|
self.features_kwargs[fn] = fe.keywords
|
|
65
123
|
|
|
66
|
-
def _validate_execution_tree(self, feature_extractors):
|
|
124
|
+
def _validate_execution_tree(self, feature_extractors: dict) -> dict:
|
|
125
|
+
"""Validate the feature dependency graph."""
|
|
67
126
|
for fname, f in feature_extractors.items():
|
|
68
127
|
f = _get_underlying_func(f)
|
|
69
128
|
pe_type = getattr(f, "parent_extractor_type", [FeatureExtractor])
|
|
70
|
-
|
|
129
|
+
if type(self) not in pe_type:
|
|
130
|
+
raise TypeError(
|
|
131
|
+
f"Feature '{fname}' cannot be a child of {type(self).__name__}"
|
|
132
|
+
)
|
|
71
133
|
return feature_extractors
|
|
72
134
|
|
|
73
|
-
def _check_is_trainable(self, feature_extractors):
|
|
74
|
-
|
|
135
|
+
def _check_is_trainable(self, feature_extractors: dict) -> bool:
|
|
136
|
+
"""Check if any of the contained features are trainable."""
|
|
75
137
|
for fname, f in feature_extractors.items():
|
|
76
138
|
if isinstance(f, FeatureExtractor):
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
if is_trainable:
|
|
83
|
-
break
|
|
84
|
-
return is_trainable
|
|
139
|
+
if f._is_trainable:
|
|
140
|
+
return True
|
|
141
|
+
elif isinstance(_get_underlying_func(f), TrainableFeature):
|
|
142
|
+
return True
|
|
143
|
+
return False
|
|
85
144
|
|
|
86
145
|
def preprocess(self, *x, **kwargs):
|
|
146
|
+
"""Apply pre-processing to the input data.
|
|
147
|
+
|
|
148
|
+
Parameters
|
|
149
|
+
----------
|
|
150
|
+
*x : tuple
|
|
151
|
+
Input data.
|
|
152
|
+
**kwargs
|
|
153
|
+
Additional keyword arguments.
|
|
154
|
+
|
|
155
|
+
Returns
|
|
156
|
+
-------
|
|
157
|
+
tuple
|
|
158
|
+
The pre-processed data.
|
|
159
|
+
|
|
160
|
+
"""
|
|
87
161
|
return (*x,)
|
|
88
162
|
|
|
89
|
-
def __call__(self, *x, _batch_size=None, _ch_names=None):
|
|
163
|
+
def __call__(self, *x, _batch_size=None, _ch_names=None) -> dict:
|
|
164
|
+
"""Apply all feature extractors to the input data.
|
|
165
|
+
|
|
166
|
+
Parameters
|
|
167
|
+
----------
|
|
168
|
+
*x : tuple
|
|
169
|
+
Input data.
|
|
170
|
+
_batch_size : int, optional
|
|
171
|
+
The number of samples in the batch.
|
|
172
|
+
_ch_names : list of str, optional
|
|
173
|
+
The names of the channels in the input data.
|
|
174
|
+
|
|
175
|
+
Returns
|
|
176
|
+
-------
|
|
177
|
+
dict
|
|
178
|
+
A dictionary where keys are feature names and values are the
|
|
179
|
+
computed feature values.
|
|
180
|
+
|
|
181
|
+
"""
|
|
90
182
|
assert _batch_size is not None
|
|
91
183
|
assert _ch_names is not None
|
|
92
184
|
if self._is_trainable:
|
|
@@ -100,59 +192,83 @@ class FeatureExtractor(TrainableFeature):
|
|
|
100
192
|
r = f(*z, _batch_size=_batch_size, _ch_names=_ch_names)
|
|
101
193
|
else:
|
|
102
194
|
r = f(*z)
|
|
103
|
-
|
|
104
|
-
if hasattr(
|
|
105
|
-
r =
|
|
195
|
+
f_und = _get_underlying_func(f)
|
|
196
|
+
if hasattr(f_und, "feature_kind"):
|
|
197
|
+
r = f_und.feature_kind(r, _ch_names=_ch_names)
|
|
106
198
|
if not isinstance(fname, str) or not fname:
|
|
107
|
-
|
|
108
|
-
fname = ""
|
|
109
|
-
else:
|
|
110
|
-
fname = f.__name__
|
|
199
|
+
fname = getattr(f_und, "__name__", "")
|
|
111
200
|
if isinstance(r, dict):
|
|
112
|
-
if fname
|
|
113
|
-
fname += "_"
|
|
201
|
+
prefix = f"{fname}_" if fname else ""
|
|
114
202
|
for k, v in r.items():
|
|
115
|
-
self._add_feature_to_dict(results_dict,
|
|
203
|
+
self._add_feature_to_dict(results_dict, prefix + k, v, _batch_size)
|
|
116
204
|
else:
|
|
117
205
|
self._add_feature_to_dict(results_dict, fname, r, _batch_size)
|
|
118
206
|
return results_dict
|
|
119
207
|
|
|
120
|
-
def _add_feature_to_dict(
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
208
|
+
def _add_feature_to_dict(
|
|
209
|
+
self, results_dict: dict, name: str, value: any, batch_size: int
|
|
210
|
+
):
|
|
211
|
+
"""Add a computed feature to the results dictionary."""
|
|
212
|
+
if isinstance(value, np.ndarray):
|
|
124
213
|
assert value.shape[0] == batch_size
|
|
125
|
-
|
|
214
|
+
results_dict[name] = value
|
|
126
215
|
|
|
127
216
|
def clear(self):
|
|
217
|
+
"""Clear the state of all trainable sub-features."""
|
|
128
218
|
if not self._is_trainable:
|
|
129
219
|
return
|
|
130
|
-
for
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
f.clear()
|
|
220
|
+
for f in self.feature_extractors_dict.values():
|
|
221
|
+
if isinstance(_get_underlying_func(f), TrainableFeature):
|
|
222
|
+
_get_underlying_func(f).clear()
|
|
134
223
|
|
|
135
224
|
def partial_fit(self, *x, y=None):
|
|
225
|
+
"""Partially fit all trainable sub-features."""
|
|
136
226
|
if not self._is_trainable:
|
|
137
227
|
return
|
|
138
228
|
z = self.preprocess(*x, **self.preprocess_kwargs)
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
229
|
+
if not isinstance(z, tuple):
|
|
230
|
+
z = (z,)
|
|
231
|
+
for f in self.feature_extractors_dict.values():
|
|
232
|
+
if isinstance(_get_underlying_func(f), TrainableFeature):
|
|
233
|
+
_get_underlying_func(f).partial_fit(*z, y=y)
|
|
143
234
|
|
|
144
235
|
def fit(self):
|
|
236
|
+
"""Fit all trainable sub-features."""
|
|
145
237
|
if not self._is_trainable:
|
|
146
238
|
return
|
|
147
|
-
for
|
|
148
|
-
|
|
149
|
-
if isinstance(f, TrainableFeature):
|
|
239
|
+
for f in self.feature_extractors_dict.values():
|
|
240
|
+
if isinstance(_get_underlying_func(f), TrainableFeature):
|
|
150
241
|
f.fit()
|
|
151
242
|
super().fit()
|
|
152
243
|
|
|
153
244
|
|
|
154
245
|
class MultivariateFeature:
|
|
155
|
-
|
|
246
|
+
"""A mixin for features that operate on multiple channels.
|
|
247
|
+
|
|
248
|
+
This class provides a `__call__` method that converts a feature array into
|
|
249
|
+
a dictionary with named features, where names are derived from channel
|
|
250
|
+
names.
|
|
251
|
+
"""
|
|
252
|
+
|
|
253
|
+
def __call__(
|
|
254
|
+
self, x: np.ndarray, _ch_names: list[str] | None = None
|
|
255
|
+
) -> dict | np.ndarray:
|
|
256
|
+
"""Convert a feature array to a named dictionary.
|
|
257
|
+
|
|
258
|
+
Parameters
|
|
259
|
+
----------
|
|
260
|
+
x : numpy.ndarray
|
|
261
|
+
The computed feature array.
|
|
262
|
+
_ch_names : list of str, optional
|
|
263
|
+
The list of channel names.
|
|
264
|
+
|
|
265
|
+
Returns
|
|
266
|
+
-------
|
|
267
|
+
dict or numpy.ndarray
|
|
268
|
+
A dictionary of named features, or the original array if feature
|
|
269
|
+
channel names cannot be generated.
|
|
270
|
+
|
|
271
|
+
"""
|
|
156
272
|
assert _ch_names is not None
|
|
157
273
|
f_channels = self.feature_channel_names(_ch_names)
|
|
158
274
|
if isinstance(x, dict):
|
|
@@ -163,37 +279,66 @@ class MultivariateFeature:
|
|
|
163
279
|
return self._array_to_dict(x, f_channels)
|
|
164
280
|
|
|
165
281
|
@staticmethod
|
|
166
|
-
def _array_to_dict(
|
|
282
|
+
def _array_to_dict(
|
|
283
|
+
x: np.ndarray, f_channels: list[str], name: str = ""
|
|
284
|
+
) -> dict | np.ndarray:
|
|
285
|
+
"""Convert a numpy array to a dictionary with named keys."""
|
|
167
286
|
assert isinstance(x, np.ndarray)
|
|
168
|
-
if
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
return {name: x}
|
|
172
|
-
return x
|
|
173
|
-
assert x.shape[1] == len(f_channels)
|
|
287
|
+
if not f_channels:
|
|
288
|
+
return {name: x} if name else x
|
|
289
|
+
assert x.shape[1] == len(f_channels), f"{x.shape[1]} != {len(f_channels)}"
|
|
174
290
|
x = x.swapaxes(0, 1)
|
|
175
|
-
|
|
291
|
+
prefix = f"{name}_" if name else ""
|
|
292
|
+
names = [f"{prefix}{ch}" for ch in f_channels]
|
|
176
293
|
return dict(zip(names, x))
|
|
177
294
|
|
|
178
|
-
def feature_channel_names(self, ch_names):
|
|
295
|
+
def feature_channel_names(self, ch_names: list[str]) -> list[str]:
|
|
296
|
+
"""Generate feature names based on channel names.
|
|
297
|
+
|
|
298
|
+
Parameters
|
|
299
|
+
----------
|
|
300
|
+
ch_names : list of str
|
|
301
|
+
The names of the input channels.
|
|
302
|
+
|
|
303
|
+
Returns
|
|
304
|
+
-------
|
|
305
|
+
list of str
|
|
306
|
+
The names for the output features.
|
|
307
|
+
|
|
308
|
+
"""
|
|
179
309
|
return []
|
|
180
310
|
|
|
181
311
|
|
|
182
312
|
class UnivariateFeature(MultivariateFeature):
|
|
183
|
-
|
|
313
|
+
"""A feature kind for operations applied to each channel independently."""
|
|
314
|
+
|
|
315
|
+
def feature_channel_names(self, ch_names: list[str]) -> list[str]:
|
|
316
|
+
"""Return the channel names themselves as feature names."""
|
|
184
317
|
return ch_names
|
|
185
318
|
|
|
186
319
|
|
|
187
320
|
class BivariateFeature(MultivariateFeature):
|
|
188
|
-
|
|
321
|
+
"""A feature kind for operations on pairs of channels.
|
|
322
|
+
|
|
323
|
+
Parameters
|
|
324
|
+
----------
|
|
325
|
+
channel_pair_format : str, default="{}<>{}"
|
|
326
|
+
A format string used to create feature names from pairs of
|
|
327
|
+
channel names.
|
|
328
|
+
|
|
329
|
+
"""
|
|
330
|
+
|
|
331
|
+
def __init__(self, *args, channel_pair_format: str = "{}<>{}"):
|
|
189
332
|
super().__init__(*args)
|
|
190
333
|
self.channel_pair_format = channel_pair_format
|
|
191
334
|
|
|
192
335
|
@staticmethod
|
|
193
|
-
def get_pair_iterators(n):
|
|
336
|
+
def get_pair_iterators(n: int) -> tuple[np.ndarray, np.ndarray]:
|
|
337
|
+
"""Get indices for unique, unordered pairs of channels."""
|
|
194
338
|
return np.triu_indices(n, 1)
|
|
195
339
|
|
|
196
|
-
def feature_channel_names(self, ch_names):
|
|
340
|
+
def feature_channel_names(self, ch_names: list[str]) -> list[str]:
|
|
341
|
+
"""Generate feature names for each pair of channels."""
|
|
197
342
|
return [
|
|
198
343
|
self.channel_pair_format.format(ch_names[i], ch_names[j])
|
|
199
344
|
for i, j in zip(*self.get_pair_iterators(len(ch_names)))
|
|
@@ -201,8 +346,11 @@ class BivariateFeature(MultivariateFeature):
|
|
|
201
346
|
|
|
202
347
|
|
|
203
348
|
class DirectedBivariateFeature(BivariateFeature):
|
|
349
|
+
"""A feature kind for directed operations on pairs of channels."""
|
|
350
|
+
|
|
204
351
|
@staticmethod
|
|
205
|
-
def get_pair_iterators(n):
|
|
352
|
+
def get_pair_iterators(n: int) -> list[np.ndarray]:
|
|
353
|
+
"""Get indices for all ordered pairs of channels (excluding self-pairs)."""
|
|
206
354
|
return [
|
|
207
355
|
np.append(a, b)
|
|
208
356
|
for a, b in zip(np.tril_indices(n, -1), np.triu_indices(n, 1))
|
|
@@ -36,8 +36,12 @@ class EntropyFeatureExtractor(FeatureExtractor):
|
|
|
36
36
|
counts_m = np.empty((*x.shape[:-1], (x.shape[-1] - m + 1) // l))
|
|
37
37
|
counts_mp1 = np.empty((*x.shape[:-1], (x.shape[-1] - m) // l))
|
|
38
38
|
for i in np.ndindex(x.shape[:-1]):
|
|
39
|
-
counts_m[
|
|
40
|
-
|
|
39
|
+
counts_m[i + (slice(None),)] = _channel_app_samp_entropy_counts(
|
|
40
|
+
x[i], m, rr[i], l
|
|
41
|
+
)
|
|
42
|
+
counts_mp1[i + (slice(None),)] = _channel_app_samp_entropy_counts(
|
|
43
|
+
x[i], m + 1, rr[i], l
|
|
44
|
+
)
|
|
41
45
|
return counts_m, counts_mp1
|
|
42
46
|
|
|
43
47
|
|
|
@@ -62,7 +66,7 @@ def complexity_sample_entropy(counts_m, counts_mp1):
|
|
|
62
66
|
def complexity_svd_entropy(x, m=10, tau=1):
|
|
63
67
|
x_emb = np.empty((*x.shape[:-1], (x.shape[-1] - m + 1) // tau, m))
|
|
64
68
|
for i in np.ndindex(x.shape[:-1]):
|
|
65
|
-
x_emb[
|
|
69
|
+
x_emb[i + (slice(None), slice(None))] = _create_embedding(x[i], m, tau)
|
|
66
70
|
s = np.linalg.svdvals(x_emb)
|
|
67
71
|
s /= s.sum(axis=-1, keepdims=True)
|
|
68
72
|
return -np.sum(s * np.log(s), axis=-1)
|
|
@@ -26,7 +26,7 @@ def dimensionality_higuchi_fractal_dim(x, k_max=10, eps=1e-7):
|
|
|
26
26
|
for i in np.ndindex(x.shape[:-1]):
|
|
27
27
|
for k in range(1, k_max + 1):
|
|
28
28
|
for m in range(k):
|
|
29
|
-
L_km[m] = np.mean(np.abs(np.diff(x[
|
|
29
|
+
L_km[m] = np.mean(np.abs(np.diff(x[i + (slice(m, None),)], n=k)))
|
|
30
30
|
L_k[k - 1] = (N - 1) * np.sum(L_km[:k]) / (k**3)
|
|
31
31
|
L_k = np.maximum(L_k, eps)
|
|
32
32
|
hfd[i] = np.linalg.lstsq(log_k, np.log(L_k))[0][0]
|
eegdash/features/inspect.py
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
import inspect
|
|
2
4
|
from collections.abc import Callable
|
|
3
5
|
|
|
@@ -5,7 +7,27 @@ from . import extractors, feature_bank
|
|
|
5
7
|
from .extractors import FeatureExtractor, MultivariateFeature, _get_underlying_func
|
|
6
8
|
|
|
7
9
|
|
|
8
|
-
def get_feature_predecessors(feature_or_extractor: Callable):
|
|
10
|
+
def get_feature_predecessors(feature_or_extractor: Callable) -> list:
|
|
11
|
+
"""Get the dependency hierarchy for a feature or feature extractor.
|
|
12
|
+
|
|
13
|
+
This function recursively traverses the `parent_extractor_type` attribute
|
|
14
|
+
of a feature or extractor to build a list representing its dependency
|
|
15
|
+
lineage.
|
|
16
|
+
|
|
17
|
+
Parameters
|
|
18
|
+
----------
|
|
19
|
+
feature_or_extractor : callable
|
|
20
|
+
The feature function or :class:`FeatureExtractor` class to inspect.
|
|
21
|
+
|
|
22
|
+
Returns
|
|
23
|
+
-------
|
|
24
|
+
list
|
|
25
|
+
A nested list representing the dependency tree. For a simple linear
|
|
26
|
+
chain, this will be a flat list from the specific feature up to the
|
|
27
|
+
base `FeatureExtractor`. For multiple dependencies, it will contain
|
|
28
|
+
tuples of sub-dependencies.
|
|
29
|
+
|
|
30
|
+
"""
|
|
9
31
|
current = _get_underlying_func(feature_or_extractor)
|
|
10
32
|
if current is FeatureExtractor:
|
|
11
33
|
return [current]
|
|
@@ -20,18 +42,59 @@ def get_feature_predecessors(feature_or_extractor: Callable):
|
|
|
20
42
|
return [current, tuple(predecessors)]
|
|
21
43
|
|
|
22
44
|
|
|
23
|
-
def get_feature_kind(feature: Callable):
|
|
45
|
+
def get_feature_kind(feature: Callable) -> MultivariateFeature:
|
|
46
|
+
"""Get the 'kind' of a feature function.
|
|
47
|
+
|
|
48
|
+
The feature kind (e.g., univariate, bivariate) is typically attached by a
|
|
49
|
+
decorator.
|
|
50
|
+
|
|
51
|
+
Parameters
|
|
52
|
+
----------
|
|
53
|
+
feature : callable
|
|
54
|
+
The feature function to inspect.
|
|
55
|
+
|
|
56
|
+
Returns
|
|
57
|
+
-------
|
|
58
|
+
MultivariateFeature
|
|
59
|
+
An instance of the feature kind (e.g., `UnivariateFeature()`).
|
|
60
|
+
|
|
61
|
+
"""
|
|
24
62
|
return _get_underlying_func(feature).feature_kind
|
|
25
63
|
|
|
26
64
|
|
|
27
|
-
def get_all_features():
|
|
65
|
+
def get_all_features() -> list[tuple[str, Callable]]:
|
|
66
|
+
"""Get a list of all available feature functions.
|
|
67
|
+
|
|
68
|
+
Scans the `eegdash.features.feature_bank` module for functions that have
|
|
69
|
+
been decorated to have a `feature_kind` attribute.
|
|
70
|
+
|
|
71
|
+
Returns
|
|
72
|
+
-------
|
|
73
|
+
list[tuple[str, callable]]
|
|
74
|
+
A list of (name, function) tuples for all discovered features.
|
|
75
|
+
|
|
76
|
+
"""
|
|
77
|
+
|
|
28
78
|
def isfeature(x):
|
|
29
79
|
return hasattr(_get_underlying_func(x), "feature_kind")
|
|
30
80
|
|
|
31
81
|
return inspect.getmembers(feature_bank, isfeature)
|
|
32
82
|
|
|
33
83
|
|
|
34
|
-
def get_all_feature_extractors():
|
|
84
|
+
def get_all_feature_extractors() -> list[tuple[str, type[FeatureExtractor]]]:
|
|
85
|
+
"""Get a list of all available `FeatureExtractor` classes.
|
|
86
|
+
|
|
87
|
+
Scans the `eegdash.features.feature_bank` module for all classes that
|
|
88
|
+
subclass :class:`~eegdash.features.extractors.FeatureExtractor`.
|
|
89
|
+
|
|
90
|
+
Returns
|
|
91
|
+
-------
|
|
92
|
+
list[tuple[str, type[FeatureExtractor]]]
|
|
93
|
+
A list of (name, class) tuples for all discovered feature extractors,
|
|
94
|
+
including the base `FeatureExtractor` itself.
|
|
95
|
+
|
|
96
|
+
"""
|
|
97
|
+
|
|
35
98
|
def isfeatureextractor(x):
|
|
36
99
|
return inspect.isclass(x) and issubclass(x, FeatureExtractor)
|
|
37
100
|
|
|
@@ -41,7 +104,19 @@ def get_all_feature_extractors():
|
|
|
41
104
|
]
|
|
42
105
|
|
|
43
106
|
|
|
44
|
-
def get_all_feature_kinds():
|
|
107
|
+
def get_all_feature_kinds() -> list[tuple[str, type[MultivariateFeature]]]:
|
|
108
|
+
"""Get a list of all available feature 'kind' classes.
|
|
109
|
+
|
|
110
|
+
Scans the `eegdash.features.extractors` module for all classes that
|
|
111
|
+
subclass :class:`~eegdash.features.extractors.MultivariateFeature`.
|
|
112
|
+
|
|
113
|
+
Returns
|
|
114
|
+
-------
|
|
115
|
+
list[tuple[str, type[MultivariateFeature]]]
|
|
116
|
+
A list of (name, class) tuples for all discovered feature kinds.
|
|
117
|
+
|
|
118
|
+
"""
|
|
119
|
+
|
|
45
120
|
def isfeaturekind(x):
|
|
46
121
|
return inspect.isclass(x) and issubclass(x, MultivariateFeature)
|
|
47
122
|
|