py-neuromodulation 0.0.4__py3-none-any.whl → 0.0.6__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- py_neuromodulation/ConnectivityDecoding/_get_grid_hull.m +34 -34
- py_neuromodulation/ConnectivityDecoding/_get_grid_whole_brain.py +95 -106
- py_neuromodulation/ConnectivityDecoding/_helper_write_connectome.py +107 -119
- py_neuromodulation/__init__.py +80 -13
- py_neuromodulation/{nm_RMAP.py → analysis/RMAP.py} +496 -531
- py_neuromodulation/analysis/__init__.py +4 -0
- py_neuromodulation/{nm_decode.py → analysis/decode.py} +918 -992
- py_neuromodulation/{nm_analysis.py → analysis/feature_reader.py} +994 -1074
- py_neuromodulation/{nm_plots.py → analysis/plots.py} +627 -612
- py_neuromodulation/{nm_stats.py → analysis/stats.py} +458 -480
- py_neuromodulation/data/README +6 -6
- py_neuromodulation/data/dataset_description.json +8 -8
- py_neuromodulation/data/participants.json +32 -32
- py_neuromodulation/data/participants.tsv +2 -2
- py_neuromodulation/data/sub-testsub/ses-EphysMedOff/ieeg/sub-testsub_ses-EphysMedOff_space-mni_coordsystem.json +5 -5
- py_neuromodulation/data/sub-testsub/ses-EphysMedOff/ieeg/sub-testsub_ses-EphysMedOff_space-mni_electrodes.tsv +11 -11
- py_neuromodulation/data/sub-testsub/ses-EphysMedOff/ieeg/sub-testsub_ses-EphysMedOff_task-gripforce_run-0_channels.tsv +11 -11
- py_neuromodulation/data/sub-testsub/ses-EphysMedOff/ieeg/sub-testsub_ses-EphysMedOff_task-gripforce_run-0_ieeg.json +18 -18
- py_neuromodulation/data/sub-testsub/ses-EphysMedOff/ieeg/sub-testsub_ses-EphysMedOff_task-gripforce_run-0_ieeg.vhdr +35 -35
- py_neuromodulation/data/sub-testsub/ses-EphysMedOff/ieeg/sub-testsub_ses-EphysMedOff_task-gripforce_run-0_ieeg.vmrk +13 -13
- py_neuromodulation/data/sub-testsub/ses-EphysMedOff/sub-testsub_ses-EphysMedOff_scans.tsv +2 -2
- py_neuromodulation/default_settings.yaml +241 -0
- py_neuromodulation/features/__init__.py +31 -0
- py_neuromodulation/features/bandpower.py +165 -0
- py_neuromodulation/features/bispectra.py +157 -0
- py_neuromodulation/features/bursts.py +297 -0
- py_neuromodulation/features/coherence.py +255 -0
- py_neuromodulation/features/feature_processor.py +121 -0
- py_neuromodulation/features/fooof.py +142 -0
- py_neuromodulation/features/hjorth_raw.py +57 -0
- py_neuromodulation/features/linelength.py +21 -0
- py_neuromodulation/features/mne_connectivity.py +148 -0
- py_neuromodulation/features/nolds.py +94 -0
- py_neuromodulation/features/oscillatory.py +249 -0
- py_neuromodulation/features/sharpwaves.py +432 -0
- py_neuromodulation/filter/__init__.py +3 -0
- py_neuromodulation/filter/kalman_filter.py +67 -0
- py_neuromodulation/filter/kalman_filter_external.py +1890 -0
- py_neuromodulation/filter/mne_filter.py +128 -0
- py_neuromodulation/filter/notch_filter.py +93 -0
- py_neuromodulation/grid_cortex.tsv +40 -40
- py_neuromodulation/liblsl/libpugixml.so.1.12 +0 -0
- py_neuromodulation/liblsl/linux/bionic_amd64/liblsl.1.16.2.so +0 -0
- py_neuromodulation/liblsl/linux/bookworm_amd64/liblsl.1.16.2.so +0 -0
- py_neuromodulation/liblsl/linux/focal_amd46/liblsl.1.16.2.so +0 -0
- py_neuromodulation/liblsl/linux/jammy_amd64/liblsl.1.16.2.so +0 -0
- py_neuromodulation/liblsl/linux/jammy_x86/liblsl.1.16.2.so +0 -0
- py_neuromodulation/liblsl/linux/noble_amd64/liblsl.1.16.2.so +0 -0
- py_neuromodulation/liblsl/macos/amd64/liblsl.1.16.2.dylib +0 -0
- py_neuromodulation/liblsl/macos/arm64/liblsl.1.16.0.dylib +0 -0
- py_neuromodulation/liblsl/windows/amd64/liblsl.1.16.2.dll +0 -0
- py_neuromodulation/liblsl/windows/x86/liblsl.1.16.2.dll +0 -0
- py_neuromodulation/processing/__init__.py +10 -0
- py_neuromodulation/{nm_artifacts.py → processing/artifacts.py} +29 -25
- py_neuromodulation/processing/data_preprocessor.py +77 -0
- py_neuromodulation/processing/filter_preprocessing.py +78 -0
- py_neuromodulation/processing/normalization.py +175 -0
- py_neuromodulation/{nm_projection.py → processing/projection.py} +370 -394
- py_neuromodulation/{nm_rereference.py → processing/rereference.py} +97 -95
- py_neuromodulation/{nm_resample.py → processing/resample.py} +56 -50
- py_neuromodulation/stream/__init__.py +3 -0
- py_neuromodulation/stream/data_processor.py +325 -0
- py_neuromodulation/stream/generator.py +53 -0
- py_neuromodulation/stream/mnelsl_player.py +94 -0
- py_neuromodulation/stream/mnelsl_stream.py +120 -0
- py_neuromodulation/stream/settings.py +292 -0
- py_neuromodulation/stream/stream.py +427 -0
- py_neuromodulation/utils/__init__.py +2 -0
- py_neuromodulation/{nm_define_nmchannels.py → utils/channels.py} +305 -302
- py_neuromodulation/utils/database.py +149 -0
- py_neuromodulation/utils/io.py +378 -0
- py_neuromodulation/utils/keyboard.py +52 -0
- py_neuromodulation/utils/logging.py +66 -0
- py_neuromodulation/utils/types.py +251 -0
- {py_neuromodulation-0.0.4.dist-info → py_neuromodulation-0.0.6.dist-info}/METADATA +28 -33
- py_neuromodulation-0.0.6.dist-info/RECORD +89 -0
- {py_neuromodulation-0.0.4.dist-info → py_neuromodulation-0.0.6.dist-info}/WHEEL +1 -1
- {py_neuromodulation-0.0.4.dist-info → py_neuromodulation-0.0.6.dist-info}/licenses/LICENSE +21 -21
- py_neuromodulation/FieldTrip.py +0 -589
- py_neuromodulation/_write_example_dataset_helper.py +0 -65
- py_neuromodulation/nm_EpochStream.py +0 -92
- py_neuromodulation/nm_IO.py +0 -417
- py_neuromodulation/nm_across_patient_decoding.py +0 -927
- py_neuromodulation/nm_bispectra.py +0 -168
- py_neuromodulation/nm_bursts.py +0 -198
- py_neuromodulation/nm_coherence.py +0 -205
- py_neuromodulation/nm_cohortwrapper.py +0 -435
- py_neuromodulation/nm_eval_timing.py +0 -239
- py_neuromodulation/nm_features.py +0 -116
- py_neuromodulation/nm_features_abc.py +0 -39
- py_neuromodulation/nm_filter.py +0 -219
- py_neuromodulation/nm_filter_preprocessing.py +0 -91
- py_neuromodulation/nm_fooof.py +0 -159
- py_neuromodulation/nm_generator.py +0 -37
- py_neuromodulation/nm_hjorth_raw.py +0 -73
- py_neuromodulation/nm_kalmanfilter.py +0 -58
- py_neuromodulation/nm_linelength.py +0 -33
- py_neuromodulation/nm_mne_connectivity.py +0 -112
- py_neuromodulation/nm_nolds.py +0 -93
- py_neuromodulation/nm_normalization.py +0 -214
- py_neuromodulation/nm_oscillatory.py +0 -448
- py_neuromodulation/nm_run_analysis.py +0 -435
- py_neuromodulation/nm_settings.json +0 -338
- py_neuromodulation/nm_settings.py +0 -68
- py_neuromodulation/nm_sharpwaves.py +0 -401
- py_neuromodulation/nm_stream_abc.py +0 -218
- py_neuromodulation/nm_stream_offline.py +0 -359
- py_neuromodulation/utils/_logging.py +0 -24
- py_neuromodulation-0.0.4.dist-info/RECORD +0 -72
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import mne
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from py_neuromodulation.utils.types import _PathLike
|
|
6
|
+
from py_neuromodulation.utils import io
|
|
7
|
+
from py_neuromodulation import logger
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class LSLOfflinePlayer:
|
|
11
|
+
def __init__(
|
|
12
|
+
self,
|
|
13
|
+
stream_name: str | None = "lsl_offline_player",
|
|
14
|
+
f_name: str | _PathLike = None,
|
|
15
|
+
raw: mne.io.Raw | None = None,
|
|
16
|
+
sfreq: int | float | None = None,
|
|
17
|
+
data: np.ndarray | None = None,
|
|
18
|
+
ch_type: str | None = "dbs",
|
|
19
|
+
) -> None:
|
|
20
|
+
"""Initialization of MNE-LSL offline player.
|
|
21
|
+
Either a filename (PathLike) is provided,
|
|
22
|
+
or data and sampling frequency to initialize an example mock-up stream.
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
Parameters
|
|
26
|
+
----------
|
|
27
|
+
stream_name : str, optional
|
|
28
|
+
LSL stream name, by default "example_stream"
|
|
29
|
+
f_name : str | None, optional
|
|
30
|
+
file name used for streaming, by default None
|
|
31
|
+
sfreq : int | float | None, optional
|
|
32
|
+
sampling rate, by default None
|
|
33
|
+
data : np.ndarray | None, optional
|
|
34
|
+
data used for streaming, by default None
|
|
35
|
+
ch_type: str | None, optional
|
|
36
|
+
channel type to select for streaming, by default "dbs"
|
|
37
|
+
|
|
38
|
+
Raises
|
|
39
|
+
------
|
|
40
|
+
ValueError
|
|
41
|
+
_description_
|
|
42
|
+
"""
|
|
43
|
+
self.sfreq = sfreq
|
|
44
|
+
self.stream_name = stream_name
|
|
45
|
+
got_raw = raw is not None
|
|
46
|
+
got_fname = f_name is not None
|
|
47
|
+
got_sfreq_data = sfreq is not None and data is not None
|
|
48
|
+
|
|
49
|
+
if not (got_fname or got_sfreq_data or got_raw):
|
|
50
|
+
error_msg = "Either f_name or raw or sfreq and data must be provided."
|
|
51
|
+
logger.critical(error_msg)
|
|
52
|
+
raise ValueError(error_msg)
|
|
53
|
+
|
|
54
|
+
if got_fname:
|
|
55
|
+
(self._path_raw, data, sfreq, line_noise, coord_list, coord_names) = (
|
|
56
|
+
io.read_BIDS_data(f_name)
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
elif got_raw:
|
|
60
|
+
self._path_raw = raw
|
|
61
|
+
|
|
62
|
+
elif got_sfreq_data:
|
|
63
|
+
info = mne.create_info(
|
|
64
|
+
ch_names=[f"ch{i}" for i in range(data.shape[0])],
|
|
65
|
+
ch_types=[ch_type for _ in range(data.shape[0])],
|
|
66
|
+
sfreq=sfreq,
|
|
67
|
+
)
|
|
68
|
+
raw = mne.io.RawArray(data, info)
|
|
69
|
+
self._path_raw = Path.cwd() / "temp_raw.fif"
|
|
70
|
+
raw.save(self._path_raw, overwrite=True)
|
|
71
|
+
|
|
72
|
+
def start_player(self, chunk_size: int = 10, n_repeat: int = 1):
|
|
73
|
+
"""Start MNE-LSL Player
|
|
74
|
+
|
|
75
|
+
Parameters
|
|
76
|
+
----------
|
|
77
|
+
chunk_size : int, optional
|
|
78
|
+
_description_, by default 1
|
|
79
|
+
n_repeat : int, optional
|
|
80
|
+
_description_, by default 1
|
|
81
|
+
"""
|
|
82
|
+
from mne_lsl.player import PlayerLSL
|
|
83
|
+
|
|
84
|
+
self.player = PlayerLSL(
|
|
85
|
+
self._path_raw,
|
|
86
|
+
name=self.stream_name,
|
|
87
|
+
chunk_size=chunk_size,
|
|
88
|
+
n_repeat=n_repeat,
|
|
89
|
+
)
|
|
90
|
+
self.player = self.player.start()
|
|
91
|
+
|
|
92
|
+
def stop_player(self):
|
|
93
|
+
"""Stop MNE-LSL Player"""
|
|
94
|
+
self.player.stop()
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
from collections.abc import Iterator
|
|
2
|
+
import time
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
import numpy as np
|
|
5
|
+
from py_neuromodulation import logger
|
|
6
|
+
from mne_lsl.lsl import resolve_streams
|
|
7
|
+
import os
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from py_neuromodulation import NMSettings
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class LSLStream:
|
|
14
|
+
"""
|
|
15
|
+
Class is used to create and connect to a LSL stream and pull data from it.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
def __init__(self, settings: "NMSettings", stream_name: str | None = None) -> None:
|
|
19
|
+
"""
|
|
20
|
+
Initialize the LSL stream.
|
|
21
|
+
|
|
22
|
+
Parameters:
|
|
23
|
+
-----------
|
|
24
|
+
settings : settings.NMSettings object
|
|
25
|
+
stream_name : str, optional
|
|
26
|
+
Name of the stream to connect to. If not provided, the first available stream is used.
|
|
27
|
+
|
|
28
|
+
Raises:
|
|
29
|
+
-------
|
|
30
|
+
RuntimeError
|
|
31
|
+
If no stream is running under the provided name or if there are multiple streams running
|
|
32
|
+
under the same name.
|
|
33
|
+
"""
|
|
34
|
+
from mne_lsl.stream import StreamLSL
|
|
35
|
+
|
|
36
|
+
self.stream: StreamLSL
|
|
37
|
+
self.keyboard_interrupt = False
|
|
38
|
+
|
|
39
|
+
self.settings = settings
|
|
40
|
+
self._n_seconds_wait_before_disconnect = 3
|
|
41
|
+
try:
|
|
42
|
+
if stream_name is None:
|
|
43
|
+
stream_name = resolve_streams()[0].name
|
|
44
|
+
logger.info(
|
|
45
|
+
f"Stream name not provided. Using first available stream: {stream_name}"
|
|
46
|
+
)
|
|
47
|
+
self.stream = StreamLSL(name=stream_name, bufsize=2).connect(timeout=2)
|
|
48
|
+
except Exception as e:
|
|
49
|
+
msg = f"Could not connect to stream: {e}. Either no stream is running under the name {stream_name} or there is several streams under this name."
|
|
50
|
+
logger.exception(msg)
|
|
51
|
+
raise RuntimeError(msg)
|
|
52
|
+
|
|
53
|
+
if self.stream.sinfo is None:
|
|
54
|
+
raise RuntimeError("Stream info is None. Check if the stream is running.")
|
|
55
|
+
|
|
56
|
+
self.winsize = settings.segment_length_features_ms / self.stream.sinfo.sfreq
|
|
57
|
+
self.sampling_interval = 1 / self.settings.sampling_rate_features_hz
|
|
58
|
+
|
|
59
|
+
# If not running the generator when the escape key is pressed.
|
|
60
|
+
self.headless: bool = not os.environ.get("DISPLAY")
|
|
61
|
+
if not self.headless:
|
|
62
|
+
from py_neuromodulation.utils.keyboard import KeyboardListener
|
|
63
|
+
|
|
64
|
+
self.listener = KeyboardListener(("esc", self.set_keyboard_interrupt))
|
|
65
|
+
self.listener.start()
|
|
66
|
+
|
|
67
|
+
def get_next_batch(self) -> Iterator[tuple[np.ndarray, np.ndarray]]:
|
|
68
|
+
self.last_time = time.time()
|
|
69
|
+
check_data = None
|
|
70
|
+
data = None
|
|
71
|
+
stream_start_time = None
|
|
72
|
+
|
|
73
|
+
while self.stream.connected:
|
|
74
|
+
time_diff = time.time() - self.last_time # in s
|
|
75
|
+
time.sleep(0.005)
|
|
76
|
+
if time_diff >= self.sampling_interval:
|
|
77
|
+
self.last_time = time.time()
|
|
78
|
+
|
|
79
|
+
logger.debug(f"Pull data - current time: {self.last_time}")
|
|
80
|
+
logger.debug(f"time since last data pull {time_diff} seconds")
|
|
81
|
+
|
|
82
|
+
if time_diff >= 2 * self.sampling_interval:
|
|
83
|
+
logger.warning(
|
|
84
|
+
"Feature computation time between two consecutive samples"
|
|
85
|
+
"was twice the feature sampling interval"
|
|
86
|
+
)
|
|
87
|
+
if data is not None:
|
|
88
|
+
check_data = data
|
|
89
|
+
|
|
90
|
+
data, timestamp = self.stream.get_data(winsize=self.winsize)
|
|
91
|
+
if stream_start_time is None:
|
|
92
|
+
stream_start_time = timestamp[0]
|
|
93
|
+
|
|
94
|
+
for i in range(self._n_seconds_wait_before_disconnect):
|
|
95
|
+
if (
|
|
96
|
+
data is not None
|
|
97
|
+
and check_data is not None
|
|
98
|
+
and np.allclose(data, check_data, atol=1e-7, rtol=1e-7)
|
|
99
|
+
):
|
|
100
|
+
logger.warning(
|
|
101
|
+
f"No new data incoming. Disconnecting stream in {3-i} seconds."
|
|
102
|
+
)
|
|
103
|
+
time.sleep(1)
|
|
104
|
+
i += 1
|
|
105
|
+
if i == self._n_seconds_wait_before_disconnect:
|
|
106
|
+
self.stream.disconnect()
|
|
107
|
+
logger.warning("Stream disconnected.")
|
|
108
|
+
break
|
|
109
|
+
|
|
110
|
+
yield timestamp, data
|
|
111
|
+
|
|
112
|
+
logger.info(f"Stream time: {timestamp[-1] - stream_start_time}")
|
|
113
|
+
|
|
114
|
+
if not self.headless and self.keyboard_interrupt:
|
|
115
|
+
logger.info("Keyboard interrupt")
|
|
116
|
+
self.listener.stop()
|
|
117
|
+
self.stream.disconnect()
|
|
118
|
+
|
|
119
|
+
def set_keyboard_interrupt(self):
|
|
120
|
+
self.keyboard_interrupt = True
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
"""Module for handling settings."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import ClassVar
|
|
5
|
+
from pydantic import Field, model_validator
|
|
6
|
+
|
|
7
|
+
from py_neuromodulation import PYNM_DIR, logger, user_features
|
|
8
|
+
|
|
9
|
+
from py_neuromodulation.utils.types import (
|
|
10
|
+
BoolSelector,
|
|
11
|
+
FrequencyRange,
|
|
12
|
+
PreprocessorName,
|
|
13
|
+
_PathLike,
|
|
14
|
+
NMBaseModel,
|
|
15
|
+
NormMethod,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
from py_neuromodulation.processing.filter_preprocessing import FilterSettings
|
|
19
|
+
from py_neuromodulation.processing.normalization import NormalizationSettings
|
|
20
|
+
from py_neuromodulation.processing.resample import ResamplerSettings
|
|
21
|
+
from py_neuromodulation.processing.projection import ProjectionSettings
|
|
22
|
+
|
|
23
|
+
from py_neuromodulation.filter import KalmanSettings
|
|
24
|
+
from py_neuromodulation.features import BispectraSettings
|
|
25
|
+
from py_neuromodulation.features import NoldsSettings
|
|
26
|
+
from py_neuromodulation.features import MNEConnectivitySettings
|
|
27
|
+
from py_neuromodulation.features import FooofSettings
|
|
28
|
+
from py_neuromodulation.features import CoherenceSettings
|
|
29
|
+
from py_neuromodulation.features import SharpwaveSettings
|
|
30
|
+
from py_neuromodulation.features import OscillatorySettings, BandPowerSettings
|
|
31
|
+
from py_neuromodulation.features import BurstsSettings
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class FeatureSelection(BoolSelector):
|
|
35
|
+
raw_hjorth: bool = True
|
|
36
|
+
return_raw: bool = True
|
|
37
|
+
bandpass_filter: bool = False
|
|
38
|
+
stft: bool = False
|
|
39
|
+
fft: bool = True
|
|
40
|
+
welch: bool = True
|
|
41
|
+
sharpwave_analysis: bool = True
|
|
42
|
+
fooof: bool = False
|
|
43
|
+
nolds: bool = False
|
|
44
|
+
coherence: bool = False
|
|
45
|
+
bursts: bool = True
|
|
46
|
+
linelength: bool = True
|
|
47
|
+
mne_connectivity: bool = False
|
|
48
|
+
bispectrum: bool = False
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class PostprocessingSettings(BoolSelector):
|
|
52
|
+
feature_normalization: bool = True
|
|
53
|
+
project_cortex: bool = False
|
|
54
|
+
project_subcortex: bool = False
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class NMSettings(NMBaseModel):
|
|
58
|
+
# Class variable to store instances
|
|
59
|
+
_instances: ClassVar[list["NMSettings"]] = []
|
|
60
|
+
|
|
61
|
+
# General settings
|
|
62
|
+
sampling_rate_features_hz: float = Field(default=10, gt=0)
|
|
63
|
+
segment_length_features_ms: float = Field(default=1000, gt=0)
|
|
64
|
+
frequency_ranges_hz: dict[str, FrequencyRange] = {
|
|
65
|
+
"theta": FrequencyRange(4, 8),
|
|
66
|
+
"alpha": FrequencyRange(8, 12),
|
|
67
|
+
"low_beta": FrequencyRange(13, 20),
|
|
68
|
+
"high_beta": FrequencyRange(20, 35),
|
|
69
|
+
"low_gamma": FrequencyRange(60, 80),
|
|
70
|
+
"high_gamma": FrequencyRange(90, 200),
|
|
71
|
+
"HFA": FrequencyRange(200, 400),
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
# Preproceessing settings
|
|
75
|
+
preprocessing: list[PreprocessorName] = [
|
|
76
|
+
"raw_resampling",
|
|
77
|
+
"notch_filter",
|
|
78
|
+
"re_referencing",
|
|
79
|
+
]
|
|
80
|
+
raw_resampling_settings: ResamplerSettings = ResamplerSettings()
|
|
81
|
+
preprocessing_filter: FilterSettings = FilterSettings()
|
|
82
|
+
raw_normalization_settings: NormalizationSettings = NormalizationSettings()
|
|
83
|
+
|
|
84
|
+
# Postprocessing settings
|
|
85
|
+
postprocessing: PostprocessingSettings = PostprocessingSettings()
|
|
86
|
+
feature_normalization_settings: NormalizationSettings = NormalizationSettings()
|
|
87
|
+
project_cortex_settings: ProjectionSettings = ProjectionSettings(max_dist_mm=20)
|
|
88
|
+
project_subcortex_settings: ProjectionSettings = ProjectionSettings(max_dist_mm=5)
|
|
89
|
+
|
|
90
|
+
# Feature settings
|
|
91
|
+
features: FeatureSelection = FeatureSelection()
|
|
92
|
+
|
|
93
|
+
fft_settings: OscillatorySettings = OscillatorySettings()
|
|
94
|
+
welch_settings: OscillatorySettings = OscillatorySettings()
|
|
95
|
+
stft_settings: OscillatorySettings = OscillatorySettings()
|
|
96
|
+
bandpass_filter_settings: BandPowerSettings = BandPowerSettings()
|
|
97
|
+
kalman_filter_settings: KalmanSettings = KalmanSettings()
|
|
98
|
+
burst_settings: BurstsSettings = BurstsSettings()
|
|
99
|
+
sharpwave_analysis_settings: SharpwaveSettings = SharpwaveSettings()
|
|
100
|
+
mne_connectivity_settings: MNEConnectivitySettings = MNEConnectivitySettings()
|
|
101
|
+
coherence_settings: CoherenceSettings = CoherenceSettings()
|
|
102
|
+
fooof_settings: FooofSettings = FooofSettings()
|
|
103
|
+
nolds_settings: NoldsSettings = NoldsSettings()
|
|
104
|
+
bispectrum_settings: BispectraSettings = BispectraSettings()
|
|
105
|
+
|
|
106
|
+
def __init__(self, *args, **kwargs) -> None:
|
|
107
|
+
super().__init__(*args, **kwargs)
|
|
108
|
+
|
|
109
|
+
for feat_name in user_features.keys():
|
|
110
|
+
setattr(self.features, feat_name, True)
|
|
111
|
+
|
|
112
|
+
NMSettings._add_instance(self)
|
|
113
|
+
|
|
114
|
+
@classmethod
|
|
115
|
+
def _add_instance(cls, instance: "NMSettings") -> None:
|
|
116
|
+
"""Keep track of all instances created in class variable"""
|
|
117
|
+
cls._instances.append(instance)
|
|
118
|
+
|
|
119
|
+
@classmethod
|
|
120
|
+
def _add_feature(cls, feature: str) -> None:
|
|
121
|
+
for instance in cls._instances:
|
|
122
|
+
setattr(instance.features, feature, True)
|
|
123
|
+
|
|
124
|
+
@classmethod
|
|
125
|
+
def _remove_feature(cls, feature: str) -> None:
|
|
126
|
+
for instance in cls._instances:
|
|
127
|
+
delattr(instance.features, feature)
|
|
128
|
+
|
|
129
|
+
@model_validator(mode="after")
|
|
130
|
+
def validate_settings(self):
|
|
131
|
+
if len(self.features.get_enabled()) == 0:
|
|
132
|
+
raise ValueError("At least one feature must be selected.")
|
|
133
|
+
|
|
134
|
+
# Replace spaces with underscores in frequency band names
|
|
135
|
+
self.frequency_ranges_hz = {
|
|
136
|
+
k.replace(" ", "_"): v for k, v in self.frequency_ranges_hz.items()
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if self.features.bandpass_filter:
|
|
140
|
+
# Check BandPass settings frequency bands
|
|
141
|
+
self.bandpass_filter_settings.validate_fbands(self)
|
|
142
|
+
|
|
143
|
+
# Check Kalman filter frequency bands
|
|
144
|
+
if self.bandpass_filter_settings.kalman_filter:
|
|
145
|
+
self.kalman_filter_settings.validate_fbands(self)
|
|
146
|
+
|
|
147
|
+
for k, v in self.frequency_ranges_hz.items():
|
|
148
|
+
if not isinstance(v, FrequencyRange):
|
|
149
|
+
self.frequency_ranges_hz[k] = FrequencyRange.create_from(v)
|
|
150
|
+
|
|
151
|
+
return self
|
|
152
|
+
|
|
153
|
+
def reset(self) -> "NMSettings":
|
|
154
|
+
self.features.disable_all()
|
|
155
|
+
self.preprocessing = []
|
|
156
|
+
self.postprocessing.disable_all()
|
|
157
|
+
return self
|
|
158
|
+
|
|
159
|
+
def set_fast_compute(self) -> "NMSettings":
|
|
160
|
+
self.reset()
|
|
161
|
+
self.features.fft = True
|
|
162
|
+
self.preprocessing = [
|
|
163
|
+
"raw_resampling",
|
|
164
|
+
"notch_filter",
|
|
165
|
+
"re_referencing",
|
|
166
|
+
]
|
|
167
|
+
self.postprocessing.feature_normalization = True
|
|
168
|
+
self.postprocessing.project_cortex = False
|
|
169
|
+
self.postprocessing.project_subcortex = False
|
|
170
|
+
|
|
171
|
+
return self
|
|
172
|
+
|
|
173
|
+
def enable_all_features(self):
|
|
174
|
+
self.features.enable_all()
|
|
175
|
+
return self
|
|
176
|
+
|
|
177
|
+
def disable_all_features(self):
|
|
178
|
+
self.features.disable_all()
|
|
179
|
+
return self
|
|
180
|
+
|
|
181
|
+
@staticmethod
|
|
182
|
+
def get_fast_compute() -> "NMSettings":
|
|
183
|
+
return NMSettings.get_default().set_fast_compute()
|
|
184
|
+
|
|
185
|
+
@classmethod
|
|
186
|
+
def load(cls, settings: "NMSettings | _PathLike | None") -> "NMSettings":
|
|
187
|
+
if isinstance(settings, cls):
|
|
188
|
+
return settings.validate()
|
|
189
|
+
if settings is None:
|
|
190
|
+
return cls.get_default()
|
|
191
|
+
return cls.from_file(str(settings))
|
|
192
|
+
|
|
193
|
+
@staticmethod
|
|
194
|
+
def from_file(PATH: _PathLike) -> "NMSettings":
|
|
195
|
+
"""Load settings from file or a directory.
|
|
196
|
+
|
|
197
|
+
Args:
|
|
198
|
+
PATH (_PathLike): Path to settings file or to directory containing settings file,
|
|
199
|
+
or path to experiment including experiment prefix
|
|
200
|
+
(e.g. /path/to/exp/exp_prefix[_SETTINGS.json]).
|
|
201
|
+
Supported file types are .json and .yaml
|
|
202
|
+
|
|
203
|
+
Raises:
|
|
204
|
+
ValueError: when file format is not supported.
|
|
205
|
+
|
|
206
|
+
Returns:
|
|
207
|
+
NMSettings: PyNM settings object
|
|
208
|
+
"""
|
|
209
|
+
path = Path(PATH)
|
|
210
|
+
|
|
211
|
+
# If directory is passed, look for settings file inside
|
|
212
|
+
if path.is_dir():
|
|
213
|
+
for child in path.iterdir():
|
|
214
|
+
if child.is_file() and child.suffix in [".json", ".yaml"]:
|
|
215
|
+
path = child
|
|
216
|
+
break
|
|
217
|
+
|
|
218
|
+
# If prefix is passed, look for settings file matching prefix
|
|
219
|
+
if not path.is_dir() and not path.is_file():
|
|
220
|
+
for child in path.parent.iterdir():
|
|
221
|
+
ext = child.suffix.lower()
|
|
222
|
+
if (
|
|
223
|
+
child.is_file()
|
|
224
|
+
and ext in [".json", ".yaml"]
|
|
225
|
+
and child.name == path.stem + "_SETTINGS" + ext
|
|
226
|
+
):
|
|
227
|
+
path = child
|
|
228
|
+
break
|
|
229
|
+
|
|
230
|
+
match path.suffix:
|
|
231
|
+
case ".json":
|
|
232
|
+
import json
|
|
233
|
+
|
|
234
|
+
with open(path) as f:
|
|
235
|
+
model_dict = json.load(f)
|
|
236
|
+
case ".yaml":
|
|
237
|
+
import yaml
|
|
238
|
+
|
|
239
|
+
# with open(path) as f:
|
|
240
|
+
# model_dict = yaml.safe_load(f)
|
|
241
|
+
|
|
242
|
+
# Timon: this is potentially dangerous since python code is directly executed
|
|
243
|
+
with open(path) as f:
|
|
244
|
+
model_dict = yaml.load(f, Loader=yaml.Loader)
|
|
245
|
+
|
|
246
|
+
case _:
|
|
247
|
+
raise ValueError("File format not supported.")
|
|
248
|
+
|
|
249
|
+
return NMSettings(**model_dict)
|
|
250
|
+
|
|
251
|
+
@staticmethod
|
|
252
|
+
def get_default() -> "NMSettings":
|
|
253
|
+
return NMSettings.from_file(PYNM_DIR / "default_settings.yaml")
|
|
254
|
+
|
|
255
|
+
@staticmethod
|
|
256
|
+
def list_normalization_methods() -> list[NormMethod]:
|
|
257
|
+
return NormalizationSettings.list_normalization_methods()
|
|
258
|
+
|
|
259
|
+
def save(
|
|
260
|
+
self, out_dir: _PathLike = ".", prefix: str = "", format: str = "yaml"
|
|
261
|
+
) -> None:
|
|
262
|
+
filename = f"{prefix}_SETTINGS.{format}" if prefix else f"SETTINGS.{format}"
|
|
263
|
+
|
|
264
|
+
path_out = Path(out_dir) / filename
|
|
265
|
+
|
|
266
|
+
with open(path_out, "w") as f:
|
|
267
|
+
match format:
|
|
268
|
+
case "json":
|
|
269
|
+
f.write(self.model_dump_json(indent=4))
|
|
270
|
+
case "yaml":
|
|
271
|
+
import yaml
|
|
272
|
+
|
|
273
|
+
yaml.dump(self.model_dump(), f, default_flow_style=None)
|
|
274
|
+
|
|
275
|
+
logger.info(f"Settings saved to {path_out.resolve()}")
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
# For retrocompatibility with previous versions of PyNM
|
|
279
|
+
def get_default_settings() -> NMSettings:
|
|
280
|
+
return NMSettings.get_default()
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def reset_settings(settings: NMSettings) -> NMSettings:
|
|
284
|
+
return settings.reset()
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def get_fast_compute() -> NMSettings:
|
|
288
|
+
return NMSettings.get_fast_compute()
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def test_settings(settings: NMSettings) -> NMSettings:
|
|
292
|
+
return settings.validate()
|