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.

@@ -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
+ """
@@ -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
- assert type(self) in pe_type
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
- is_trainable = False
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
- is_trainable = f._is_trainable
78
- else:
79
- f = _get_underlying_func(f)
80
- if isinstance(f, TrainableFeature):
81
- is_trainable = True
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
- f = _get_underlying_func(f)
104
- if hasattr(f, "feature_kind"):
105
- r = f.feature_kind(r, _ch_names=_ch_names)
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
- if isinstance(f, FeatureExtractor) or not hasattr(f, "__name__"):
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, fname + k, v, _batch_size)
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(self, results_dict, name, value, batch_size):
121
- if not isinstance(value, np.ndarray):
122
- results_dict[name] = value
123
- else:
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
- results_dict[name] = value
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 fname, f in self.feature_extractors_dict.items():
131
- f = _get_underlying_func(f)
132
- if isinstance(f, TrainableFeature):
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
- for fname, f in self.feature_extractors_dict.items():
140
- f = _get_underlying_func(f)
141
- if isinstance(f, TrainableFeature):
142
- f.partial_fit(*z, y=y)
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 fname, f in self.feature_extractors_dict.items():
148
- f = _get_underlying_func(f)
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
- def __call__(self, x, _ch_names=None):
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(x, f_channels, name=""):
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 len(f_channels) == 0:
169
- assert x.ndim == 1
170
- if name:
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
- names = [f"{name}_{ch}" for ch in f_channels] if name else f_channels
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
- def feature_channel_names(self, ch_names):
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
- def __init__(self, *args, channel_pair_format="{}<>{}"):
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[*i, :] = _channel_app_samp_entropy_counts(x[i], m, rr[i], l)
40
- counts_mp1[*i, :] = _channel_app_samp_entropy_counts(x[i], m + 1, rr[i], l)
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[*i, :, :] = _create_embedding(x[i], m, tau)
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[*i, m:], n=k)))
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]
@@ -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