py-neuromodulation 0.0.4__py3-none-any.whl → 0.0.6__py3-none-any.whl

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