py-neuromodulation 0.0.5__py3-none-any.whl → 0.0.7__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/__init__.py +16 -10
- py_neuromodulation/{nm_RMAP.py → analysis/RMAP.py} +2 -2
- py_neuromodulation/analysis/__init__.py +4 -0
- py_neuromodulation/{nm_decode.py → analysis/decode.py} +4 -4
- py_neuromodulation/{nm_analysis.py → analysis/feature_reader.py} +21 -20
- py_neuromodulation/{nm_plots.py → analysis/plots.py} +54 -12
- py_neuromodulation/{nm_stats.py → analysis/stats.py} +2 -8
- py_neuromodulation/{nm_settings.yaml → default_settings.yaml} +7 -9
- py_neuromodulation/features/__init__.py +31 -0
- py_neuromodulation/features/bandpower.py +165 -0
- py_neuromodulation/{nm_bispectra.py → features/bispectra.py} +11 -12
- py_neuromodulation/{nm_bursts.py → features/bursts.py} +14 -9
- py_neuromodulation/{nm_coherence.py → features/coherence.py} +28 -19
- py_neuromodulation/{nm_features.py → features/feature_processor.py} +30 -53
- py_neuromodulation/{nm_fooof.py → features/fooof.py} +11 -8
- py_neuromodulation/{nm_hjorth_raw.py → features/hjorth_raw.py} +10 -5
- py_neuromodulation/{nm_linelength.py → features/linelength.py} +1 -1
- py_neuromodulation/{nm_mne_connectivity.py → features/mne_connectivity.py} +5 -6
- py_neuromodulation/{nm_nolds.py → features/nolds.py} +5 -7
- py_neuromodulation/{nm_oscillatory.py → features/oscillatory.py} +7 -181
- py_neuromodulation/{nm_sharpwaves.py → features/sharpwaves.py} +13 -4
- py_neuromodulation/filter/__init__.py +3 -0
- py_neuromodulation/{nm_kalmanfilter.py → filter/kalman_filter.py} +67 -71
- py_neuromodulation/filter/kalman_filter_external.py +1890 -0
- py_neuromodulation/{nm_filter.py → filter/mne_filter.py} +128 -219
- py_neuromodulation/filter/notch_filter.py +93 -0
- py_neuromodulation/processing/__init__.py +10 -0
- py_neuromodulation/{nm_artifacts.py → processing/artifacts.py} +2 -3
- py_neuromodulation/{nm_preprocessing.py → processing/data_preprocessor.py} +19 -25
- py_neuromodulation/{nm_filter_preprocessing.py → processing/filter_preprocessing.py} +3 -4
- py_neuromodulation/{nm_normalization.py → processing/normalization.py} +9 -7
- py_neuromodulation/{nm_projection.py → processing/projection.py} +14 -14
- py_neuromodulation/{nm_rereference.py → processing/rereference.py} +13 -13
- py_neuromodulation/{nm_resample.py → processing/resample.py} +1 -4
- py_neuromodulation/stream/__init__.py +3 -0
- py_neuromodulation/{nm_run_analysis.py → stream/data_processor.py} +42 -42
- py_neuromodulation/stream/generator.py +53 -0
- py_neuromodulation/{nm_mnelsl_generator.py → stream/mnelsl_player.py} +10 -6
- py_neuromodulation/{nm_mnelsl_stream.py → stream/mnelsl_stream.py} +13 -9
- py_neuromodulation/{nm_settings.py → stream/settings.py} +27 -24
- py_neuromodulation/{nm_stream.py → stream/stream.py} +217 -188
- py_neuromodulation/utils/__init__.py +2 -0
- py_neuromodulation/{nm_define_nmchannels.py → utils/channels.py} +14 -9
- py_neuromodulation/{nm_database.py → utils/database.py} +2 -2
- py_neuromodulation/{nm_IO.py → utils/io.py} +42 -77
- py_neuromodulation/utils/keyboard.py +52 -0
- py_neuromodulation/{nm_logger.py → utils/logging.py} +3 -3
- py_neuromodulation/{nm_types.py → utils/types.py} +72 -14
- {py_neuromodulation-0.0.5.dist-info → py_neuromodulation-0.0.7.dist-info}/METADATA +12 -29
- py_neuromodulation-0.0.7.dist-info/RECORD +89 -0
- py_neuromodulation/FieldTrip.py +0 -589
- py_neuromodulation/_write_example_dataset_helper.py +0 -83
- py_neuromodulation/nm_generator.py +0 -45
- py_neuromodulation/nm_stream_abc.py +0 -166
- py_neuromodulation-0.0.5.dist-info/RECORD +0 -83
- {py_neuromodulation-0.0.5.dist-info → py_neuromodulation-0.0.7.dist-info}/WHEEL +0 -0
- {py_neuromodulation-0.0.5.dist-info → py_neuromodulation-0.0.7.dist-info}/licenses/LICENSE +0 -0
py_neuromodulation/__init__.py
CHANGED
|
@@ -2,13 +2,13 @@ import os
|
|
|
2
2
|
import platform
|
|
3
3
|
from pathlib import PurePath
|
|
4
4
|
from importlib.metadata import version
|
|
5
|
-
from .
|
|
5
|
+
from py_neuromodulation.utils.logging import NMLogger
|
|
6
6
|
|
|
7
7
|
#####################################
|
|
8
8
|
# Globals and environment variables #
|
|
9
9
|
#####################################
|
|
10
10
|
|
|
11
|
-
__version__ = version(
|
|
11
|
+
__version__ = version("py_neuromodulation") # get version from pyproject.toml
|
|
12
12
|
|
|
13
13
|
# Check if the module is running headless (no display) for tests and doc builds
|
|
14
14
|
PYNM_HEADLESS: bool = not os.environ.get("DISPLAY")
|
|
@@ -64,11 +64,17 @@ logger = NMLogger(__name__) # logger initialization first to prevent circular i
|
|
|
64
64
|
####################################
|
|
65
65
|
# API: Exposed classes and methods #
|
|
66
66
|
####################################
|
|
67
|
-
from .
|
|
68
|
-
from .
|
|
69
|
-
from .
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
67
|
+
from .stream.stream import Stream
|
|
68
|
+
from .stream.data_processor import DataProcessor
|
|
69
|
+
from .stream.settings import NMSettings
|
|
70
|
+
|
|
71
|
+
from .analysis.feature_reader import FeatureReader
|
|
72
|
+
from .features.feature_processor import add_custom_feature, remove_custom_feature
|
|
73
|
+
|
|
74
|
+
from .utils import types
|
|
75
|
+
from .utils import io
|
|
76
|
+
|
|
77
|
+
from . import stream
|
|
78
|
+
from . import analysis
|
|
79
|
+
|
|
80
|
+
from .stream.settings import get_default_settings, get_fast_compute, reset_settings
|
|
@@ -8,8 +8,8 @@ import pandas as pd
|
|
|
8
8
|
import nibabel as nib
|
|
9
9
|
from matplotlib import pyplot as plt
|
|
10
10
|
|
|
11
|
-
from py_neuromodulation.
|
|
12
|
-
from py_neuromodulation.
|
|
11
|
+
from py_neuromodulation.plots import reg_plot
|
|
12
|
+
from py_neuromodulation.types import _PathLike
|
|
13
13
|
from py_neuromodulation import PYNM_DIR
|
|
14
14
|
|
|
15
15
|
LIST_STRUC_UNCONNECTED_GRIDPOINTS_HULL = [256, 385, 417, 447, 819, 914]
|
|
@@ -65,11 +65,11 @@ class Decoder:
|
|
|
65
65
|
label: np.ndarray | None = None,
|
|
66
66
|
label_name: str | None = None,
|
|
67
67
|
used_chs: list[str] = [],
|
|
68
|
-
model
|
|
68
|
+
model=LinearRegression(),
|
|
69
69
|
eval_method: Callable = r2_score,
|
|
70
|
-
cv_method
|
|
70
|
+
cv_method=model_selection.KFold(n_splits=3, shuffle=False),
|
|
71
71
|
use_nested_cv: bool = False,
|
|
72
|
-
threshold_score
|
|
72
|
+
threshold_score=True,
|
|
73
73
|
mov_detection_threshold: float = 0.5,
|
|
74
74
|
TRAIN_VAL_SPLIT: bool = False,
|
|
75
75
|
RUN_BAY_OPT: bool = False,
|
|
@@ -89,7 +89,7 @@ class Decoder:
|
|
|
89
89
|
model_save: bool = False,
|
|
90
90
|
) -> None:
|
|
91
91
|
"""Initialize here a feature file for processing
|
|
92
|
-
Read settings.json
|
|
92
|
+
Read settings.json channels.csv and features.csv
|
|
93
93
|
Read target label
|
|
94
94
|
|
|
95
95
|
Parameters
|
|
@@ -10,10 +10,11 @@ from sklearn.model_selection import KFold
|
|
|
10
10
|
|
|
11
11
|
from scipy.stats import zscore as scipy_zscore
|
|
12
12
|
|
|
13
|
-
from py_neuromodulation import
|
|
14
|
-
from py_neuromodulation.
|
|
15
|
-
from py_neuromodulation.
|
|
16
|
-
from py_neuromodulation.
|
|
13
|
+
from py_neuromodulation.utils import io
|
|
14
|
+
from py_neuromodulation.analysis import plots
|
|
15
|
+
from py_neuromodulation.analysis.decode import Decoder
|
|
16
|
+
from py_neuromodulation.utils.types import _PathLike
|
|
17
|
+
from py_neuromodulation.stream.settings import NMSettings
|
|
17
18
|
|
|
18
19
|
|
|
19
20
|
target_filter_str = {
|
|
@@ -46,14 +47,14 @@ class FeatureReader:
|
|
|
46
47
|
|
|
47
48
|
"""
|
|
48
49
|
self.feature_dir = feature_dir
|
|
49
|
-
self.feature_list: list[str] =
|
|
50
|
+
self.feature_list: list[str] = io.get_run_list_indir(self.feature_dir)
|
|
50
51
|
self.feature_file = feature_file if feature_file else self.feature_list[0]
|
|
51
52
|
|
|
52
53
|
FILE_BASENAME = PurePath(self.feature_file).stem
|
|
53
54
|
PATH_READ_FILE = str(PurePath(self.feature_dir, FILE_BASENAME, FILE_BASENAME))
|
|
54
55
|
|
|
55
56
|
self.settings = NMSettings.from_file(PATH_READ_FILE)
|
|
56
|
-
self.sidecar =
|
|
57
|
+
self.sidecar = io.read_sidecar(PATH_READ_FILE)
|
|
57
58
|
if self.sidecar["sess_right"] is None:
|
|
58
59
|
if "coords" in self.sidecar:
|
|
59
60
|
if len(self.sidecar["coords"]["cortex_left"]["ch_names"]) > 0:
|
|
@@ -61,22 +62,22 @@ class FeatureReader:
|
|
|
61
62
|
if len(self.sidecar["coords"]["cortex_right"]["ch_names"]) > 0:
|
|
62
63
|
self.sidecar["sess_right"] = True
|
|
63
64
|
self.sfreq = self.sidecar["sfreq"]
|
|
64
|
-
self.
|
|
65
|
-
self.feature_arr =
|
|
65
|
+
self.channels = io.read_channels(PATH_READ_FILE)
|
|
66
|
+
self.feature_arr = io.read_features(PATH_READ_FILE)
|
|
66
67
|
|
|
67
|
-
self.ch_names = self.
|
|
68
|
+
self.ch_names = self.channels.new_name
|
|
68
69
|
self.used_chs = list(
|
|
69
|
-
self.
|
|
70
|
-
(self.
|
|
70
|
+
self.channels[
|
|
71
|
+
(self.channels["target"] == 0) & (self.channels["used"] == 1)
|
|
71
72
|
]["new_name"]
|
|
72
73
|
)
|
|
73
|
-
self.ch_names_ECOG = self.
|
|
74
|
+
self.ch_names_ECOG = self.channels.query(
|
|
74
75
|
'(type=="ecog") and (used == 1) and (status=="good")'
|
|
75
76
|
).new_name.to_list()
|
|
76
77
|
|
|
77
78
|
# init plotter
|
|
78
|
-
self.nmplotter =
|
|
79
|
-
if self.
|
|
79
|
+
self.nmplotter = plots.NM_Plot()
|
|
80
|
+
if self.channels["target"].sum() > 0:
|
|
80
81
|
self.label_name = self._get_target_ch()
|
|
81
82
|
self.label = self.read_target_ch(
|
|
82
83
|
self.feature_arr,
|
|
@@ -86,7 +87,7 @@ class FeatureReader:
|
|
|
86
87
|
)
|
|
87
88
|
|
|
88
89
|
def _get_target_ch(self) -> str:
|
|
89
|
-
target_names = list(self.
|
|
90
|
+
target_names = list(self.channels[self.channels["target"] == 1]["name"])
|
|
90
91
|
target_clean = [
|
|
91
92
|
target_name
|
|
92
93
|
for target_name in target_names
|
|
@@ -312,7 +313,7 @@ class FeatureReader:
|
|
|
312
313
|
threshold=threshold,
|
|
313
314
|
)
|
|
314
315
|
|
|
315
|
-
|
|
316
|
+
plots.plot_epochs_avg(
|
|
316
317
|
X_epoch=X_epoch,
|
|
317
318
|
y_epoch=y_epoch,
|
|
318
319
|
epoch_len=epoch_len,
|
|
@@ -376,7 +377,7 @@ class FeatureReader:
|
|
|
376
377
|
else:
|
|
377
378
|
df = self.feature_arr[self.feature_arr.columns[::-1]]
|
|
378
379
|
|
|
379
|
-
|
|
380
|
+
plots.plot_all_features(
|
|
380
381
|
df=df,
|
|
381
382
|
time_limit_low_s=time_limit_low_s,
|
|
382
383
|
time_limit_high_s=time_limit_high_s,
|
|
@@ -517,12 +518,12 @@ class FeatureReader:
|
|
|
517
518
|
def plot_feature_series_time(
|
|
518
519
|
self,
|
|
519
520
|
):
|
|
520
|
-
|
|
521
|
+
plots.plot_feature_series_time(self.feature_arr)
|
|
521
522
|
|
|
522
523
|
def plot_corr_matrix(
|
|
523
524
|
self,
|
|
524
525
|
):
|
|
525
|
-
return
|
|
526
|
+
return plots.plot_corr_matrix(
|
|
526
527
|
self.feature_arr,
|
|
527
528
|
)
|
|
528
529
|
|
|
@@ -963,7 +964,7 @@ class FeatureReader:
|
|
|
963
964
|
)
|
|
964
965
|
|
|
965
966
|
if save_results:
|
|
966
|
-
|
|
967
|
+
io.save_general_dict(
|
|
967
968
|
dict_=performance_dict,
|
|
968
969
|
path_out=PATH_OUT,
|
|
969
970
|
prefix=folder_name,
|
|
@@ -1,13 +1,11 @@
|
|
|
1
1
|
import numpy as np
|
|
2
2
|
import pandas as pd
|
|
3
|
-
from scipy.stats import zscore as scipy_zscore
|
|
4
3
|
from matplotlib import pyplot as plt
|
|
5
4
|
from matplotlib import gridspec
|
|
6
5
|
import seaborn as sb
|
|
7
6
|
from pathlib import PurePath
|
|
8
|
-
|
|
9
|
-
from py_neuromodulation.
|
|
10
|
-
from py_neuromodulation import logger
|
|
7
|
+
from py_neuromodulation import logger, PYNM_DIR
|
|
8
|
+
from py_neuromodulation.utils.types import _PathLike
|
|
11
9
|
|
|
12
10
|
|
|
13
11
|
def plot_df_subjects(
|
|
@@ -83,8 +81,10 @@ def plot_epoch(
|
|
|
83
81
|
str_label: str = "",
|
|
84
82
|
ytick_labelsize: float | None = None,
|
|
85
83
|
):
|
|
84
|
+
from scipy.stats import zscore
|
|
85
|
+
|
|
86
86
|
if z_score is None:
|
|
87
|
-
X_epoch =
|
|
87
|
+
X_epoch = zscore(
|
|
88
88
|
np.nan_to_num(np.nanmean(np.squeeze(X_epoch), axis=0)),
|
|
89
89
|
axis=0,
|
|
90
90
|
nan_policy="omit",
|
|
@@ -128,9 +128,8 @@ def plot_epoch(
|
|
|
128
128
|
def reg_plot(
|
|
129
129
|
x_col: str, y_col: str, data: pd.DataFrame, out_path_save: str | None = None
|
|
130
130
|
):
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
131
|
+
from py_neuromodulation.analysis.stats import permutationTestSpearmansRho
|
|
132
|
+
|
|
134
133
|
plt.figure(figsize=(4, 4), dpi=300)
|
|
135
134
|
rho, p = permutationTestSpearmansRho(
|
|
136
135
|
data[x_col],
|
|
@@ -307,6 +306,8 @@ def plot_epochs_avg(
|
|
|
307
306
|
figsize_x: float = 8,
|
|
308
307
|
figsize_y: float = 8,
|
|
309
308
|
) -> None:
|
|
309
|
+
from scipy.stats import zscore
|
|
310
|
+
|
|
310
311
|
# cut channel name of for axis + "_" for more dense plot
|
|
311
312
|
if not feature_names:
|
|
312
313
|
if cut_ch_name_cols and None not in (ch_name, feature_names):
|
|
@@ -315,7 +316,7 @@ def plot_epochs_avg(
|
|
|
315
316
|
]
|
|
316
317
|
|
|
317
318
|
if normalize_data:
|
|
318
|
-
X_epoch_mean =
|
|
319
|
+
X_epoch_mean = zscore(
|
|
319
320
|
np.nanmean(np.squeeze(X_epoch), axis=0), axis=0, nan_policy="omit"
|
|
320
321
|
).T
|
|
321
322
|
else:
|
|
@@ -424,6 +425,8 @@ def plot_all_features(
|
|
|
424
425
|
OUT_PATH: _PathLike = "",
|
|
425
426
|
feature_file: str = "",
|
|
426
427
|
):
|
|
428
|
+
from scipy.stats import zscore
|
|
429
|
+
|
|
427
430
|
if time_limit_high_s is not None:
|
|
428
431
|
df = df[df["time"] < time_limit_high_s * 1000]
|
|
429
432
|
if time_limit_low_s is not None:
|
|
@@ -431,7 +434,7 @@ def plot_all_features(
|
|
|
431
434
|
|
|
432
435
|
cols_plt = [c for c in df.columns if c != "time"]
|
|
433
436
|
if normalize:
|
|
434
|
-
data_plt =
|
|
437
|
+
data_plt = zscore(df[cols_plt], nan_policy="omit")
|
|
435
438
|
else:
|
|
436
439
|
data_plt = df[cols_plt]
|
|
437
440
|
|
|
@@ -460,6 +463,47 @@ def plot_all_features(
|
|
|
460
463
|
plt.savefig(plt_path, bbox_inches="tight")
|
|
461
464
|
|
|
462
465
|
|
|
466
|
+
def read_plot_modules(
|
|
467
|
+
PATH_PLOT: _PathLike = PYNM_DIR / "plots",
|
|
468
|
+
):
|
|
469
|
+
"""Read required .mat files for plotting
|
|
470
|
+
|
|
471
|
+
Parameters
|
|
472
|
+
----------
|
|
473
|
+
PATH_PLOT : regexp, optional
|
|
474
|
+
path to plotting files, by default
|
|
475
|
+
"""
|
|
476
|
+
from py_neuromodulation.utils.io import loadmat
|
|
477
|
+
|
|
478
|
+
faces = loadmat(PurePath(PATH_PLOT, "faces.mat"))
|
|
479
|
+
vertices = loadmat(PurePath(PATH_PLOT, "Vertices.mat"))
|
|
480
|
+
grid = loadmat(PurePath(PATH_PLOT, "grid.mat"))["grid"]
|
|
481
|
+
stn_surf = loadmat(PurePath(PATH_PLOT, "STN_surf.mat"))
|
|
482
|
+
x_ver = stn_surf["vertices"][::2, 0]
|
|
483
|
+
y_ver = stn_surf["vertices"][::2, 1]
|
|
484
|
+
x_ecog = vertices["Vertices"][::1, 0]
|
|
485
|
+
y_ecog = vertices["Vertices"][::1, 1]
|
|
486
|
+
z_ecog = vertices["Vertices"][::1, 2]
|
|
487
|
+
x_stn = stn_surf["vertices"][::1, 0]
|
|
488
|
+
y_stn = stn_surf["vertices"][::1, 1]
|
|
489
|
+
z_stn = stn_surf["vertices"][::1, 2]
|
|
490
|
+
|
|
491
|
+
return (
|
|
492
|
+
faces,
|
|
493
|
+
vertices,
|
|
494
|
+
grid,
|
|
495
|
+
stn_surf,
|
|
496
|
+
x_ver,
|
|
497
|
+
y_ver,
|
|
498
|
+
x_ecog,
|
|
499
|
+
y_ecog,
|
|
500
|
+
z_ecog,
|
|
501
|
+
x_stn,
|
|
502
|
+
y_stn,
|
|
503
|
+
z_stn,
|
|
504
|
+
)
|
|
505
|
+
|
|
506
|
+
|
|
463
507
|
class NM_Plot:
|
|
464
508
|
def __init__(
|
|
465
509
|
self,
|
|
@@ -475,8 +519,6 @@ class NM_Plot:
|
|
|
475
519
|
self.sess_right = sess_right
|
|
476
520
|
self.proj_matrix_cortex = proj_matrix_cortex
|
|
477
521
|
|
|
478
|
-
from py_neuromodulation.nm_IO import read_plot_modules
|
|
479
|
-
|
|
480
522
|
(
|
|
481
523
|
self.faces,
|
|
482
524
|
self.vertices,
|
|
@@ -9,11 +9,6 @@ import pandas as pd
|
|
|
9
9
|
import scipy.stats as stats
|
|
10
10
|
|
|
11
11
|
|
|
12
|
-
def fitlm(x, y):
|
|
13
|
-
import statsmodels.api as sm
|
|
14
|
-
return sm.OLS(y, sm.add_constant(x)).fit()
|
|
15
|
-
|
|
16
|
-
|
|
17
12
|
def fitlm_kfold(x, y, kfold_splits=5):
|
|
18
13
|
from sklearn.linear_model import LinearRegression
|
|
19
14
|
from sklearn.model_selection import KFold
|
|
@@ -28,7 +23,6 @@ def fitlm_kfold(x, y, kfold_splits=5):
|
|
|
28
23
|
for i, (train, test) in enumerate(kfold.split(x, y)):
|
|
29
24
|
model.fit(x.iloc[train, :], y.iloc[train, :])
|
|
30
25
|
score = model.score(x.iloc[test, :], y.iloc[test, :])
|
|
31
|
-
# mdl = fitlm(np.squeeze(y.iloc[test,:].transpose()), np.squeeze(model.predict(x.iloc[test, :])))
|
|
32
26
|
scores.append(score)
|
|
33
27
|
coeffs = np.vstack((coeffs, model.coef_))
|
|
34
28
|
coeffs = list(np.delete(coeffs, 0))
|
|
@@ -303,9 +297,9 @@ def cluster_wise_p_val_correction(p_arr, p_sig=0.05, num_permutations=10000):
|
|
|
303
297
|
p (float) : significance level of highest cluster
|
|
304
298
|
p_min_index : indices of significant samples
|
|
305
299
|
"""
|
|
306
|
-
from
|
|
300
|
+
from scipy.ndimage import label as measure_label
|
|
307
301
|
|
|
308
|
-
labels, num_clusters = measure_label(p_arr <= p_sig
|
|
302
|
+
labels, num_clusters = measure_label(p_arr <= p_sig)
|
|
309
303
|
|
|
310
304
|
# loop through clusters of p_val series or image
|
|
311
305
|
index_cluster = {}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
---
|
|
2
|
-
|
|
3
2
|
########################
|
|
4
3
|
### General settings ###
|
|
5
4
|
########################
|
|
@@ -15,7 +14,6 @@ frequency_ranges_hz: # frequency band ranges can be added, removed and altered
|
|
|
15
14
|
high_gamma: [90, 200]
|
|
16
15
|
HFA: [200, 400]
|
|
17
16
|
|
|
18
|
-
|
|
19
17
|
# Enabled features
|
|
20
18
|
features:
|
|
21
19
|
raw_hjorth: true
|
|
@@ -62,7 +60,6 @@ preprocessing_filter:
|
|
|
62
60
|
lowpass_filter_cutoff_hz: 200
|
|
63
61
|
highpass_filter_cutoff_hz: 3
|
|
64
62
|
|
|
65
|
-
|
|
66
63
|
################################
|
|
67
64
|
### Postprocessing settings ####
|
|
68
65
|
################################
|
|
@@ -82,7 +79,6 @@ project_cortex_settings:
|
|
|
82
79
|
project_subcortex_settings:
|
|
83
80
|
max_dist_mm: 5
|
|
84
81
|
|
|
85
|
-
|
|
86
82
|
#################################
|
|
87
83
|
### Feature specific settings ###
|
|
88
84
|
#################################
|
|
@@ -154,6 +150,7 @@ sharpwave_analysis_settings:
|
|
|
154
150
|
sharpwave_features:
|
|
155
151
|
peak_left: false
|
|
156
152
|
peak_right: false
|
|
153
|
+
num_peaks: false
|
|
157
154
|
trough: false
|
|
158
155
|
width: false
|
|
159
156
|
prominence: true
|
|
@@ -185,7 +182,7 @@ sharpwave_analysis_settings:
|
|
|
185
182
|
var: []
|
|
186
183
|
apply_estimator_between_peaks_and_troughs: true
|
|
187
184
|
|
|
188
|
-
|
|
185
|
+
coherence_settings:
|
|
189
186
|
channels: [] # List of channel pairs, empty by default. Each pair is a list of two channels.
|
|
190
187
|
# Example channels: [[STN_RIGHT_0, ECOG_RIGHT_0], [STN_RIGHT_1, ECOG_RIGHT_1]]
|
|
191
188
|
frequency_bands: [high_beta]
|
|
@@ -196,8 +193,9 @@ coherence:
|
|
|
196
193
|
method:
|
|
197
194
|
coh: true
|
|
198
195
|
icoh: true
|
|
196
|
+
nperseg: 128
|
|
199
197
|
|
|
200
|
-
|
|
198
|
+
fooof_settings:
|
|
201
199
|
aperiodic:
|
|
202
200
|
exponent: true
|
|
203
201
|
offset: true
|
|
@@ -214,7 +212,7 @@ fooof:
|
|
|
214
212
|
freq_range_hz: [2, 40]
|
|
215
213
|
knee: true
|
|
216
214
|
|
|
217
|
-
|
|
215
|
+
nolds_settings:
|
|
218
216
|
sample_entropy: false
|
|
219
217
|
correlation_dimension: false
|
|
220
218
|
lyapunov_exponent: true
|
|
@@ -224,11 +222,11 @@ nolds_features:
|
|
|
224
222
|
raw: true
|
|
225
223
|
frequency_bands: [low_beta]
|
|
226
224
|
|
|
227
|
-
|
|
225
|
+
mne_connectiviy_settings:
|
|
228
226
|
method: plv
|
|
229
227
|
mode: multitaper
|
|
230
228
|
|
|
231
|
-
|
|
229
|
+
bispectrum_settings:
|
|
232
230
|
f1s: [5, 35]
|
|
233
231
|
f2s: [5, 35]
|
|
234
232
|
compute_features_for_whole_fband_range: true
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# Expose feature settings
|
|
2
|
+
from py_neuromodulation.features.bispectra import BispectraSettings
|
|
3
|
+
from py_neuromodulation.features.coherence import CoherenceSettings
|
|
4
|
+
from py_neuromodulation.features.fooof import FooofSettings
|
|
5
|
+
from py_neuromodulation.features.mne_connectivity import MNEConnectivitySettings
|
|
6
|
+
from py_neuromodulation.features.nolds import NoldsSettings
|
|
7
|
+
from py_neuromodulation.features.sharpwaves import SharpwaveSettings
|
|
8
|
+
from py_neuromodulation.features.bursts import BurstsSettings
|
|
9
|
+
from py_neuromodulation.features.oscillatory import OscillatorySettings
|
|
10
|
+
from py_neuromodulation.features.bandpower import BandPowerSettings
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
# Expose feature classes
|
|
14
|
+
from py_neuromodulation.features.linelength import LineLength
|
|
15
|
+
from py_neuromodulation.features.hjorth_raw import Hjorth, Raw
|
|
16
|
+
from py_neuromodulation.features.bispectra import Bispectra
|
|
17
|
+
from py_neuromodulation.features.coherence import Coherence
|
|
18
|
+
from py_neuromodulation.features.fooof import FooofAnalyzer
|
|
19
|
+
from py_neuromodulation.features.mne_connectivity import MNEConnectivity
|
|
20
|
+
from py_neuromodulation.features.nolds import Nolds
|
|
21
|
+
from py_neuromodulation.features.sharpwaves import SharpwaveAnalyzer
|
|
22
|
+
from py_neuromodulation.features.bursts import Bursts
|
|
23
|
+
from py_neuromodulation.features.oscillatory import FFT, STFT, Welch
|
|
24
|
+
from py_neuromodulation.features.bandpower import BandPower
|
|
25
|
+
|
|
26
|
+
# Expose feature processor and custom feature functions
|
|
27
|
+
from py_neuromodulation.features.feature_processor import (
|
|
28
|
+
FeatureProcessors,
|
|
29
|
+
add_custom_feature,
|
|
30
|
+
remove_custom_feature,
|
|
31
|
+
)
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
from collections.abc import Sequence
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
from pydantic import field_validator
|
|
5
|
+
|
|
6
|
+
from py_neuromodulation.utils.types import NMBaseModel, BoolSelector, NMFeature
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from py_neuromodulation.stream.settings import NMSettings
|
|
10
|
+
from py_neuromodulation.filter import KalmanSettings
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class BandpowerFeatures(BoolSelector):
|
|
14
|
+
activity: bool = True
|
|
15
|
+
mobility: bool = False
|
|
16
|
+
complexity: bool = False
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class BandPowerSettings(NMBaseModel):
|
|
20
|
+
segment_lengths_ms: dict[str, int] = {
|
|
21
|
+
"theta": 1000,
|
|
22
|
+
"alpha": 500,
|
|
23
|
+
"low beta": 333,
|
|
24
|
+
"high beta": 333,
|
|
25
|
+
"low gamma": 100,
|
|
26
|
+
"high gamma": 100,
|
|
27
|
+
"HFA": 100,
|
|
28
|
+
}
|
|
29
|
+
bandpower_features: BandpowerFeatures = BandpowerFeatures()
|
|
30
|
+
log_transform: bool = True
|
|
31
|
+
kalman_filter: bool = False
|
|
32
|
+
|
|
33
|
+
@field_validator("bandpower_features")
|
|
34
|
+
@classmethod
|
|
35
|
+
def bandpower_features_validator(cls, bandpower_features: BandpowerFeatures):
|
|
36
|
+
assert (
|
|
37
|
+
len(bandpower_features.get_enabled()) > 0
|
|
38
|
+
), "Set at least one bandpower_feature to True."
|
|
39
|
+
|
|
40
|
+
return bandpower_features
|
|
41
|
+
|
|
42
|
+
def validate_fbands(self, settings: "NMSettings") -> None:
|
|
43
|
+
for fband_name, seg_length_fband in self.segment_lengths_ms.items():
|
|
44
|
+
assert seg_length_fband <= settings.segment_length_features_ms, (
|
|
45
|
+
f"segment length {seg_length_fband} needs to be smaller than "
|
|
46
|
+
f" settings['segment_length_features_ms'] = {settings.segment_length_features_ms}"
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
for fband_name in settings.frequency_ranges_hz.keys():
|
|
50
|
+
assert fband_name in self.segment_lengths_ms, (
|
|
51
|
+
f"frequency range {fband_name} "
|
|
52
|
+
"needs to be defined in settings.bandpass_filter_settings.segment_lengths_ms]"
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class BandPower(NMFeature):
|
|
57
|
+
def __init__(
|
|
58
|
+
self,
|
|
59
|
+
settings: "NMSettings",
|
|
60
|
+
ch_names: Sequence[str],
|
|
61
|
+
sfreq: float,
|
|
62
|
+
use_kf: bool | None = None,
|
|
63
|
+
) -> None:
|
|
64
|
+
settings.validate()
|
|
65
|
+
|
|
66
|
+
self.bp_settings: BandPowerSettings = settings.bandpass_filter_settings
|
|
67
|
+
self.kalman_filter_settings: KalmanSettings = settings.kalman_filter_settings
|
|
68
|
+
self.sfreq = sfreq
|
|
69
|
+
self.ch_names = ch_names
|
|
70
|
+
self.KF_dict: dict = {}
|
|
71
|
+
|
|
72
|
+
from py_neuromodulation.filter import MNEFilter
|
|
73
|
+
|
|
74
|
+
self.bandpass_filter = MNEFilter(
|
|
75
|
+
f_ranges=[
|
|
76
|
+
tuple(frange) for frange in settings.frequency_ranges_hz.values()
|
|
77
|
+
],
|
|
78
|
+
sfreq=self.sfreq,
|
|
79
|
+
filter_length=self.sfreq - 1,
|
|
80
|
+
verbose=False,
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
if use_kf or (use_kf is None and self.bp_settings.kalman_filter):
|
|
84
|
+
self.init_KF("bandpass_activity")
|
|
85
|
+
|
|
86
|
+
seglengths = self.bp_settings.segment_lengths_ms
|
|
87
|
+
|
|
88
|
+
self.feature_params = []
|
|
89
|
+
for ch_idx, ch_name in enumerate(self.ch_names):
|
|
90
|
+
for f_band_idx, f_band in enumerate(settings.frequency_ranges_hz.keys()):
|
|
91
|
+
seglength_ms = seglengths[f_band]
|
|
92
|
+
seglen = int(np.floor(self.sfreq / 1000 * seglength_ms))
|
|
93
|
+
for bp_feature in self.bp_settings.bandpower_features.get_enabled():
|
|
94
|
+
feature_name = "_".join([ch_name, "bandpass", bp_feature, f_band])
|
|
95
|
+
self.feature_params.append(
|
|
96
|
+
(
|
|
97
|
+
ch_idx,
|
|
98
|
+
f_band_idx,
|
|
99
|
+
seglen,
|
|
100
|
+
bp_feature,
|
|
101
|
+
feature_name,
|
|
102
|
+
)
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
def init_KF(self, feature: str) -> None:
|
|
106
|
+
from py_neuromodulation.filter import define_KF
|
|
107
|
+
|
|
108
|
+
for f_band in self.kalman_filter_settings.frequency_bands:
|
|
109
|
+
for channel in self.ch_names:
|
|
110
|
+
self.KF_dict["_".join([channel, feature, f_band])] = define_KF(
|
|
111
|
+
self.kalman_filter_settings.Tp,
|
|
112
|
+
self.kalman_filter_settings.sigma_w,
|
|
113
|
+
self.kalman_filter_settings.sigma_v,
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
def update_KF(self, feature_calc: np.floating, KF_name: str) -> np.floating:
|
|
117
|
+
if KF_name in self.KF_dict:
|
|
118
|
+
self.KF_dict[KF_name].predict()
|
|
119
|
+
self.KF_dict[KF_name].update(feature_calc)
|
|
120
|
+
feature_calc = self.KF_dict[KF_name].x[0]
|
|
121
|
+
return feature_calc
|
|
122
|
+
|
|
123
|
+
def calc_feature(self, data: np.ndarray) -> dict:
|
|
124
|
+
data = self.bandpass_filter.filter_data(data)
|
|
125
|
+
|
|
126
|
+
feature_results = {}
|
|
127
|
+
for (
|
|
128
|
+
ch_idx,
|
|
129
|
+
f_band_idx,
|
|
130
|
+
seglen,
|
|
131
|
+
bp_feature,
|
|
132
|
+
feature_name,
|
|
133
|
+
) in self.feature_params:
|
|
134
|
+
feature_results[feature_name] = self.calc_bp_feature(
|
|
135
|
+
bp_feature, feature_name, data[ch_idx, f_band_idx, -seglen:]
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
return feature_results
|
|
139
|
+
|
|
140
|
+
def calc_bp_feature(self, bp_feature, feature_name, data):
|
|
141
|
+
match bp_feature:
|
|
142
|
+
case "activity":
|
|
143
|
+
feature_calc = np.var(data)
|
|
144
|
+
if self.bp_settings.log_transform:
|
|
145
|
+
feature_calc = np.log10(feature_calc)
|
|
146
|
+
if self.KF_dict:
|
|
147
|
+
feature_calc = self.update_KF(feature_calc, feature_name)
|
|
148
|
+
case "mobility":
|
|
149
|
+
feature_calc = np.sqrt(np.var(np.diff(data)) / np.var(data))
|
|
150
|
+
case "complexity":
|
|
151
|
+
feature_calc = self.calc_complexity(data)
|
|
152
|
+
case _:
|
|
153
|
+
raise ValueError(f"Unknown bandpower feature: {bp_feature}")
|
|
154
|
+
|
|
155
|
+
return np.nan_to_num(feature_calc)
|
|
156
|
+
|
|
157
|
+
@staticmethod
|
|
158
|
+
def calc_complexity(data: np.ndarray) -> float:
|
|
159
|
+
dat_deriv = np.diff(data)
|
|
160
|
+
deriv_variance = np.var(dat_deriv)
|
|
161
|
+
mobility = np.sqrt(deriv_variance / np.var(data))
|
|
162
|
+
dat_deriv_2_var = np.var(np.diff(dat_deriv))
|
|
163
|
+
deriv_mobility = np.sqrt(dat_deriv_2_var / deriv_variance)
|
|
164
|
+
|
|
165
|
+
return deriv_mobility / mobility
|