py-neuromodulation 0.0.2__py3-none-any.whl → 0.0.4__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 (51) hide show
  1. py_neuromodulation/ConnectivityDecoding/Automated Anatomical Labeling 3 (Rolls 2020).nii +0 -0
  2. py_neuromodulation/ConnectivityDecoding/_get_grid_hull.m +34 -0
  3. py_neuromodulation/ConnectivityDecoding/_get_grid_whole_brain.py +106 -0
  4. py_neuromodulation/ConnectivityDecoding/_helper_write_connectome.py +119 -0
  5. py_neuromodulation/ConnectivityDecoding/mni_coords_cortical_surface.mat +0 -0
  6. py_neuromodulation/ConnectivityDecoding/mni_coords_whole_brain.mat +0 -0
  7. py_neuromodulation/ConnectivityDecoding/rmap_func_all.nii +0 -0
  8. py_neuromodulation/ConnectivityDecoding/rmap_struc.nii +0 -0
  9. py_neuromodulation/{helper.py → _write_example_dataset_helper.py} +1 -1
  10. py_neuromodulation/nm_EpochStream.py +2 -3
  11. py_neuromodulation/nm_IO.py +43 -70
  12. py_neuromodulation/nm_RMAP.py +308 -11
  13. py_neuromodulation/nm_analysis.py +1 -1
  14. py_neuromodulation/nm_artifacts.py +25 -0
  15. py_neuromodulation/nm_bispectra.py +64 -29
  16. py_neuromodulation/nm_bursts.py +44 -30
  17. py_neuromodulation/nm_coherence.py +2 -1
  18. py_neuromodulation/nm_features.py +4 -2
  19. py_neuromodulation/nm_filter.py +63 -32
  20. py_neuromodulation/nm_filter_preprocessing.py +91 -0
  21. py_neuromodulation/nm_fooof.py +47 -29
  22. py_neuromodulation/nm_mne_connectivity.py +1 -1
  23. py_neuromodulation/nm_normalization.py +50 -74
  24. py_neuromodulation/nm_oscillatory.py +151 -31
  25. py_neuromodulation/nm_plots.py +13 -10
  26. py_neuromodulation/nm_rereference.py +10 -8
  27. py_neuromodulation/nm_run_analysis.py +28 -13
  28. py_neuromodulation/nm_settings.json +51 -3
  29. py_neuromodulation/nm_sharpwaves.py +103 -136
  30. py_neuromodulation/nm_stats.py +44 -30
  31. py_neuromodulation/nm_stream_abc.py +18 -10
  32. py_neuromodulation/nm_stream_offline.py +188 -46
  33. py_neuromodulation/utils/_logging.py +24 -0
  34. {py_neuromodulation-0.0.2.dist-info → py_neuromodulation-0.0.4.dist-info}/METADATA +72 -32
  35. py_neuromodulation-0.0.4.dist-info/RECORD +72 -0
  36. {py_neuromodulation-0.0.2.dist-info → py_neuromodulation-0.0.4.dist-info}/WHEEL +1 -1
  37. py_neuromodulation/data/derivatives/sub-testsub_ses-EphysMedOff_task-gripforce_run-0/MOV_aligned_features_ch_ECOG_RIGHT_0_all.png +0 -0
  38. py_neuromodulation/data/derivatives/sub-testsub_ses-EphysMedOff_task-gripforce_run-0/all_feature_plt.pdf +0 -0
  39. py_neuromodulation/data/derivatives/sub-testsub_ses-EphysMedOff_task-gripforce_run-0/sub-testsub_ses-EphysMedOff_task-gripforce_run-0_FEATURES.csv +0 -182
  40. py_neuromodulation/data/derivatives/sub-testsub_ses-EphysMedOff_task-gripforce_run-0/sub-testsub_ses-EphysMedOff_task-gripforce_run-0_LM_ML_RES.p +0 -0
  41. py_neuromodulation/data/derivatives/sub-testsub_ses-EphysMedOff_task-gripforce_run-0/sub-testsub_ses-EphysMedOff_task-gripforce_run-0_SETTINGS.json +0 -273
  42. py_neuromodulation/data/derivatives/sub-testsub_ses-EphysMedOff_task-gripforce_run-0/sub-testsub_ses-EphysMedOff_task-gripforce_run-0_SIDECAR.json +0 -6
  43. py_neuromodulation/data/derivatives/sub-testsub_ses-EphysMedOff_task-gripforce_run-0/sub-testsub_ses-EphysMedOff_task-gripforce_run-0_decoding_performance.png +0 -0
  44. py_neuromodulation/data/derivatives/sub-testsub_ses-EphysMedOff_task-gripforce_run-0/sub-testsub_ses-EphysMedOff_task-gripforce_run-0_nm_channels.csv +0 -11
  45. py_neuromodulation/py_neuromodulation.egg-info/PKG-INFO +0 -104
  46. py_neuromodulation/py_neuromodulation.egg-info/dependency_links.txt +0 -1
  47. py_neuromodulation/py_neuromodulation.egg-info/requires.txt +0 -26
  48. py_neuromodulation/py_neuromodulation.egg-info/top_level.txt +0 -1
  49. py_neuromodulation-0.0.2.dist-info/RECORD +0 -73
  50. /py_neuromodulation/{py_neuromodulation.egg-info/SOURCES.txt → utils/__init__.py} +0 -0
  51. {py_neuromodulation-0.0.2.dist-info → py_neuromodulation-0.0.4.dist-info/licenses}/LICENSE +0 -0
@@ -1,6 +1,7 @@
1
1
  import enum
2
2
  import numpy as np
3
3
  from typing import Iterable
4
+ from scipy import signal
4
5
 
5
6
  from py_neuromodulation import nm_features_abc, nm_filter
6
7
 
@@ -9,12 +10,16 @@ class Burst(nm_features_abc.Feature):
9
10
  def __init__(
10
11
  self, settings: dict, ch_names: Iterable[str], sfreq: float
11
12
  ) -> None:
12
-
13
13
  self.s = settings
14
14
  self.sfreq = sfreq
15
15
  self.ch_names = ch_names
16
16
  self.threshold = self.s["burst_settings"]["threshold"]
17
17
  self.time_duration_s = self.s["burst_settings"]["time_duration_s"]
18
+ self.samples_overlap = int(
19
+ self.sfreq
20
+ * (self.s["segment_length_features_ms"] / 1000)
21
+ / self.s["sampling_rate_features_hz"]
22
+ )
18
23
 
19
24
  self.fband_names = self.s["burst_settings"]["frequency_bands"]
20
25
  self.f_ranges = [
@@ -34,9 +39,11 @@ class Burst(nm_features_abc.Feature):
34
39
  )
35
40
  ).astype(int)
36
41
 
37
- self.num_max_samples_ring_buffer = int(self.sfreq * self.time_duration_s)
42
+ self.num_max_samples_ring_buffer = int(
43
+ self.sfreq * self.time_duration_s
44
+ )
38
45
 
39
- self.bandpass_filter = nm_filter.BandPassFilter(
46
+ self.bandpass_filter = nm_filter.MNEFilter(
40
47
  f_ranges=self.f_ranges,
41
48
  sfreq=self.sfreq,
42
49
  filter_length=self.sfreq - 1,
@@ -62,35 +69,40 @@ class Burst(nm_features_abc.Feature):
62
69
  ch_names: Iterable[str],
63
70
  sfreq: int | float,
64
71
  ):
65
- assert (
66
- isinstance(settings["burst_settings"]["threshold"], (float, int))
72
+ assert isinstance(
73
+ settings["burst_settings"]["threshold"], (float, int)
67
74
  ), f"burst settings threshold needs to be type int or float, got: {settings['burst_settings']['threshold']}"
68
75
  assert (
69
76
  0 < settings["burst_settings"]["threshold"] < 100
70
77
  ), f"burst setting threshold needs to be between 0 and 100, got: {settings['burst_settings']['threshold']}"
71
- assert (
72
- isinstance(settings["burst_settings"]["time_duration_s"], (float, int))
78
+ assert isinstance(
79
+ settings["burst_settings"]["time_duration_s"], (float, int)
73
80
  ), f"burst settings time_duration_s needs to be type int or float, got: {settings['burst_settings']['time_duration_s']}"
74
81
  assert (
75
82
  settings["burst_settings"]["time_duration_s"] > 0
76
83
  ), f"burst setting time_duration_s needs to be greater than 0, got: {settings['burst_settings']['time_duration_s']}"
77
84
 
78
85
  for fband_burst in settings["burst_settings"]["frequency_bands"]:
79
- assert (
80
- fband_burst in list(settings["frequency_ranges_hz"].keys())
86
+ assert fband_burst in list(
87
+ settings["frequency_ranges_hz"].keys()
81
88
  ), f"bursting {fband_burst} needs to be defined in settings['frequency_ranges_hz']"
82
89
 
83
- for burst_feature in settings["burst_settings"]["burst_features"].keys():
90
+ for burst_feature in settings["burst_settings"][
91
+ "burst_features"
92
+ ].keys():
84
93
  assert isinstance(
85
- settings["burst_settings"]["burst_features"][burst_feature], bool
94
+ settings["burst_settings"]["burst_features"][burst_feature],
95
+ bool,
86
96
  ), (
87
97
  f"bursting feature {burst_feature} needs to be type bool, "
88
98
  f"got: {settings['burst_settings']['burst_features'][burst_feature]}"
89
99
  )
90
- def calc_feature(self, data: np.array, features_compute: dict) -> dict:
91
100
 
101
+ def calc_feature(self, data: np.array, features_compute: dict) -> dict:
92
102
  # filter_data returns (n_channels, n_fbands, n_samples)
93
- filtered_data = self.bandpass_filter.filter_data(data)
103
+ filtered_data = np.abs(
104
+ signal.hilbert(self.bandpass_filter.filter_data(data), axis=2)
105
+ )
94
106
  for ch_idx, ch_name in enumerate(self.ch_names):
95
107
  for fband_idx, fband_name in enumerate(self.fband_names):
96
108
  new_dat = filtered_data[ch_idx, fband_idx, :]
@@ -98,8 +110,12 @@ class Burst(nm_features_abc.Feature):
98
110
  self.data_buffer[ch_name][fband_name] = new_dat
99
111
  else:
100
112
  self.data_buffer[ch_name][fband_name] = np.concatenate(
101
- (self.data_buffer[ch_name][fband_name], new_dat), axis=0
102
- )[-self.num_max_samples_ring_buffer:]
113
+ (
114
+ self.data_buffer[ch_name][fband_name],
115
+ new_dat[-self.samples_overlap :],
116
+ ),
117
+ axis=0,
118
+ )[-self.num_max_samples_ring_buffer :]
103
119
 
104
120
  # calc features
105
121
  burst_thr = np.percentile(
@@ -145,9 +161,9 @@ class Burst(nm_features_abc.Feature):
145
161
  if self.data_buffer[ch_name][fband_name][-1] > burst_thr:
146
162
  in_burst = True
147
163
 
148
- features_compute[
149
- f"{ch_name}_bursts_{fband_name}_in_burst"
150
- ] = in_burst
164
+ features_compute[f"{ch_name}_bursts_{fband_name}_in_burst"] = (
165
+ in_burst
166
+ )
151
167
  return features_compute
152
168
 
153
169
  @staticmethod
@@ -160,25 +176,23 @@ class Burst(nm_features_abc.Feature):
160
176
  bursts = np.zeros((beta_averp_norm.shape[0] + 1), dtype=bool)
161
177
  bursts[1:] = beta_averp_norm >= burst_thr
162
178
  deriv = np.diff(bursts)
163
- isburst = False
164
179
  burst_length = []
165
180
  burst_amplitude = []
166
- burst_start = 0
167
181
 
168
- for index, burst_state in enumerate(deriv):
169
- if burst_state == True:
170
- if isburst == True:
171
- burst_length.append(index - burst_start)
172
- burst_amplitude.append(beta_averp_norm[burst_start:index])
182
+ burst_time_points = np.where(deriv == True)[0]
173
183
 
174
- isburst = False
175
- else:
176
- burst_start = index
177
- isburst = True
184
+ for i in range(burst_time_points.size // 2):
185
+ burst_length.append(
186
+ burst_time_points[2 * i + 1] - burst_time_points[2 * i]
187
+ )
188
+ burst_amplitude.append(
189
+ beta_averp_norm[
190
+ burst_time_points[2 * i] : burst_time_points[2 * i + 1]
191
+ ]
192
+ )
178
193
 
179
194
  # the last burst length (in case isburst == True) is omitted,
180
195
  # since the true burst length cannot be estimated
181
-
182
196
  burst_length = np.array(burst_length) / sfreq
183
197
 
184
198
  return burst_amplitude, burst_length
@@ -111,7 +111,7 @@ class CoherenceObject:
111
111
 
112
112
  class NM_Coherence(nm_features_abc.Feature):
113
113
 
114
- coherence_objects: Iterable[CoherenceObject] = []
114
+
115
115
 
116
116
  def __init__(
117
117
  self, settings: dict, ch_names: Iterable[str], sfreq: float
@@ -119,6 +119,7 @@ class NM_Coherence(nm_features_abc.Feature):
119
119
  self.s = settings
120
120
  self.sfreq = sfreq
121
121
  self.ch_names = ch_names
122
+ self.coherence_objects: Iterable[CoherenceObject] = []
122
123
 
123
124
  for idx_coh in range(len(self.s["coherence"]["channels"])):
124
125
  fband_names = self.s["coherence"]["frequency_bands"]
@@ -11,7 +11,7 @@ from py_neuromodulation import (
11
11
  nm_oscillatory,
12
12
  nm_bursts,
13
13
  nm_linelength,
14
- nm_bispectra
14
+ nm_bispectra,
15
15
  )
16
16
 
17
17
 
@@ -56,6 +56,8 @@ class Features:
56
56
  FeatureClass = nm_oscillatory.STFT
57
57
  case "fft":
58
58
  FeatureClass = nm_oscillatory.FFT
59
+ case "welch":
60
+ FeatureClass = nm_oscillatory.Welch
59
61
  case "sharpwave_analysis":
60
62
  FeatureClass = nm_sharpwaves.SharpwaveAnalyzer
61
63
  case "fooof":
@@ -111,4 +113,4 @@ class Features:
111
113
  features_compute,
112
114
  )
113
115
 
114
- return features_compute
116
+ return features_compute
@@ -1,16 +1,23 @@
1
1
  """Module for filter functionality."""
2
+
3
+ import logging
4
+
5
+ logger = logging.getLogger("PynmLogger")
6
+
2
7
  import mne
3
8
  from mne.filter import _overlap_add_filter
4
9
  import numpy as np
5
10
 
6
11
 
7
- class BandPassFilter:
8
- """Bandpass filters data in given frequency ranges.
12
+ class MNEFilter:
13
+ """mne.filter wrapper
9
14
 
10
15
  This class stores for given frequency band ranges the filter
11
16
  coefficients with length "filter_len".
12
17
  The filters can then be used sequentially for band power estimation with
13
18
  apply_filter().
19
+ Note that this filter can be a bandpass, bandstop, lowpass, or highpass filter
20
+ depending on the frequency ranges given (see further details in mne.filter.create_filter).
14
21
 
15
22
  Parameters
16
23
  ----------
@@ -35,7 +42,7 @@ class BandPassFilter:
35
42
 
36
43
  def __init__(
37
44
  self,
38
- f_ranges: list[list[int | float | None]],
45
+ f_ranges: list[list[int | float | None]] | list[int | float | None],
39
46
  sfreq: int | float,
40
47
  filter_length: str | float = "999ms",
41
48
  l_trans_bandwidth: int | float | str = 4,
@@ -43,22 +50,36 @@ class BandPassFilter:
43
50
  verbose: bool | int | str | None = None,
44
51
  ) -> None:
45
52
  filter_bank = []
46
- # mne create_filter function only accepts str and int
53
+ # mne create_filter function only accepts str and int for filter_length
47
54
  if isinstance(filter_length, float):
48
55
  filter_length = int(filter_length)
49
56
 
57
+ if not isinstance(f_ranges[0], list):
58
+ f_ranges = [f_ranges]
59
+
50
60
  for f_range in f_ranges:
51
- filt = mne.filter.create_filter(
52
- None,
53
- sfreq,
54
- l_freq=f_range[0],
55
- h_freq=f_range[1],
56
- fir_design="firwin",
57
- l_trans_bandwidth=l_trans_bandwidth, # type: ignore
58
- h_trans_bandwidth=h_trans_bandwidth, # type: ignore
59
- filter_length=filter_length, # type: ignore
60
- verbose=verbose,
61
- )
61
+ try:
62
+ filt = mne.filter.create_filter(
63
+ None,
64
+ sfreq,
65
+ l_freq=f_range[0],
66
+ h_freq=f_range[1],
67
+ fir_design="firwin",
68
+ l_trans_bandwidth=l_trans_bandwidth, # type: ignore
69
+ h_trans_bandwidth=h_trans_bandwidth, # type: ignore
70
+ filter_length=filter_length, # type: ignore
71
+ verbose=verbose,
72
+ )
73
+ except:
74
+ filt = mne.filter.create_filter(
75
+ None,
76
+ sfreq,
77
+ l_freq=f_range[0],
78
+ h_freq=f_range[1],
79
+ fir_design="firwin",
80
+ verbose=verbose,
81
+ # filter_length=filter_length,
82
+ )
62
83
  filter_bank.append(filt)
63
84
  self.filter_bank = np.vstack(filter_bank)
64
85
 
@@ -77,10 +98,6 @@ class BandPassFilter:
77
98
  np.ndarray, shape (n_channels, n_fbands, n_samples)
78
99
  Filtered data.
79
100
 
80
- Raises
81
- ------
82
- ValueError
83
- If data.ndim > 2
84
101
  """
85
102
  if data.ndim > 2:
86
103
  raise ValueError(
@@ -89,15 +106,29 @@ class BandPassFilter:
89
106
  )
90
107
  if data.ndim == 1:
91
108
  data = np.expand_dims(data, axis=0)
109
+
92
110
  filtered = np.array(
93
111
  [
94
112
  [
95
- np.convolve(flt, chan, mode="same")
96
- for flt in self.filter_bank
113
+ np.convolve(filt, chan, mode="same")
114
+ for filt in self.filter_bank
97
115
  ]
98
116
  for chan in data
99
117
  ]
100
118
  )
119
+
120
+ # ensure here that the output dimension matches the input dimension
121
+ if data.shape[1] != filtered.shape[-1]:
122
+ # select the middle part of the filtered data
123
+ middle_index = filtered.shape[-1] // 2
124
+ filtered = filtered[
125
+ :,
126
+ :,
127
+ middle_index
128
+ - data.shape[1] // 2 : middle_index
129
+ + data.shape[1] // 2,
130
+ ]
131
+
101
132
  return filtered
102
133
 
103
134
 
@@ -125,7 +156,7 @@ class NotchFilter:
125
156
  # Code is copied from filter.py notch_filter
126
157
  if freqs.size == 0:
127
158
  self.filter_bank = None
128
- print(
159
+ logger.warning(
129
160
  "WARNING: notch_filter is activated but data is not being"
130
161
  f" filtered. This may be due to a low sampling frequency or"
131
162
  f" incorrect specifications. Make sure your settings are"
@@ -170,19 +201,19 @@ class NotchFilter:
170
201
  phase="zero",
171
202
  fir_window="hamming",
172
203
  fir_design="firwin",
173
- verbose=False
204
+ verbose=False,
174
205
  )
175
206
 
176
207
  def process(self, data: np.ndarray) -> np.ndarray:
177
208
  if self.filter_bank is None:
178
209
  return data
179
210
  return _overlap_add_filter(
180
- x=data,
181
- h=self.filter_bank,
182
- n_fft=None,
183
- phase="zero",
184
- picks=None,
185
- n_jobs=1,
186
- copy=True,
187
- pad="reflect_limited",
188
- )
211
+ x=data,
212
+ h=self.filter_bank,
213
+ n_fft=None,
214
+ phase="zero",
215
+ picks=None,
216
+ n_jobs=1,
217
+ copy=True,
218
+ pad="reflect_limited",
219
+ )
@@ -0,0 +1,91 @@
1
+ import numpy as np
2
+
3
+ from py_neuromodulation import nm_filter
4
+
5
+
6
+ class PreprocessingFilter:
7
+
8
+ def __init__(self, settings: dict, sfreq: int | float) -> None:
9
+ self.s = settings
10
+ self.sfreq = sfreq
11
+ self.filters = []
12
+
13
+ if self.s["preprocessing_filter"]["bandstop_filter"] is True:
14
+ self.filters.append(
15
+ nm_filter.MNEFilter(
16
+ f_ranges=[
17
+ self.s["preprocessing_filter"][
18
+ "bandstop_filter_settings"
19
+ ]["frequency_high_hz"],
20
+ self.s["preprocessing_filter"][
21
+ "bandstop_filter_settings"
22
+ ]["frequency_low_hz"],
23
+ ],
24
+ sfreq=self.sfreq,
25
+ filter_length=self.sfreq - 1,
26
+ verbose=False,
27
+ )
28
+ )
29
+
30
+ if self.s["preprocessing_filter"]["bandpass_filter"] is True:
31
+ self.filters.append(
32
+ nm_filter.MNEFilter(
33
+ f_ranges=[
34
+ self.s["preprocessing_filter"][
35
+ "bandpass_filter_settings"
36
+ ]["frequency_low_hz"],
37
+ self.s["preprocessing_filter"][
38
+ "bandpass_filter_settings"
39
+ ]["frequency_high_hz"],
40
+ ],
41
+ sfreq=self.sfreq,
42
+ filter_length=self.sfreq - 1,
43
+ verbose=False,
44
+ )
45
+ )
46
+ if self.s["preprocessing_filter"]["lowpass_filter"] is True:
47
+ self.filters.append(
48
+ nm_filter.MNEFilter(
49
+ f_ranges=[
50
+ None,
51
+ self.s["preprocessing_filter"][
52
+ "lowpass_filter_settings"
53
+ ]["frequency_cutoff_hz"],
54
+ ],
55
+ sfreq=self.sfreq,
56
+ filter_length=self.sfreq - 1,
57
+ verbose=False,
58
+ )
59
+ )
60
+ if self.s["preprocessing_filter"]["highpass_filter"] is True:
61
+ self.filters.append(
62
+ nm_filter.MNEFilter(
63
+ f_ranges=[
64
+ self.s["preprocessing_filter"][
65
+ "highpass_filter_settings"
66
+ ]["frequency_cutoff_hz"],
67
+ None,
68
+ ],
69
+ sfreq=self.sfreq,
70
+ filter_length=self.sfreq - 1,
71
+ verbose=False,
72
+ )
73
+ )
74
+
75
+ def process(self, data: np.ndarray) -> np.ndarray:
76
+ """Preprocess data according to the initialized list of PreprocessingFilter objects
77
+
78
+ Args:
79
+ data (numpy ndarray) :
80
+ shape(n_channels, n_samples) - data to be preprocessed.
81
+
82
+ Returns:
83
+ preprocessed_data (numpy ndarray):
84
+ shape(n_channels, n_samples) - preprocessed data
85
+ """
86
+
87
+ for filter in self.filters:
88
+ data = filter.filter_data(
89
+ data if len(data.shape) == 2 else data[:, 0, :]
90
+ )
91
+ return data if len(data.shape) == 2 else data[:, 0, :]
@@ -1,7 +1,9 @@
1
+ import logging
2
+ from typing import Iterable
3
+
1
4
  import numpy as np
2
5
  from fooof import FOOOF
3
6
  from scipy import fft
4
- from typing import Iterable
5
7
 
6
8
  from py_neuromodulation import nm_features_abc
7
9
 
@@ -18,15 +20,6 @@ class FooofAnalyzer(nm_features_abc.Feature):
18
20
  self.ap_mode = "knee" if self.settings_fooof["knee"] else "fixed"
19
21
  self.max_n_peaks = self.settings_fooof["max_n_peaks"]
20
22
 
21
- self.fm = FOOOF(
22
- aperiodic_mode=self.ap_mode,
23
- peak_width_limits=self.settings_fooof["peak_width_limits"],
24
- max_n_peaks=self.settings_fooof["max_n_peaks"],
25
- min_peak_height=self.settings_fooof["min_peak_height"],
26
- peak_threshold=self.settings_fooof["peak_threshold"],
27
- verbose=False,
28
- )
29
-
30
23
  self.num_samples = int(
31
24
  self.settings_fooof["windowlength_ms"] * sfreq / 1000
32
25
  )
@@ -75,44 +68,70 @@ class FooofAnalyzer(nm_features_abc.Feature):
75
68
  spectrum = self._get_spectrum(data[ch_idx, :])
76
69
 
77
70
  try:
78
- self.fm.fit(self.f_vec, spectrum, self.freq_range)
71
+ fm = FOOOF(
72
+ aperiodic_mode=self.ap_mode,
73
+ peak_width_limits=self.settings_fooof["peak_width_limits"],
74
+ max_n_peaks=self.settings_fooof["max_n_peaks"],
75
+ min_peak_height=self.settings_fooof["min_peak_height"],
76
+ peak_threshold=self.settings_fooof["peak_threshold"],
77
+ verbose=False,
78
+ )
79
+ fm.fit(self.f_vec, spectrum, self.freq_range)
79
80
  except Exception as e:
80
- print(e)
81
- print(f"failing spectrum: {spectrum}")
81
+ logging.critical(e, exc_info=True)
82
+
83
+ if fm.fooofed_spectrum_ is None:
84
+ FIT_PASSED = False
85
+ else:
86
+ FIT_PASSED = True
87
+
82
88
  if self.settings_fooof["aperiodic"]["exponent"]:
83
89
  features_compute[f"{ch_name}_fooof_a_exp"] = (
84
- np.nan_to_num(self.fm.get_params("aperiodic_params", "exponent"))
85
- if self.fm.fooofed_spectrum_ is not None
90
+ np.nan_to_num(fm.get_params("aperiodic_params", "exponent"))
91
+ if FIT_PASSED is True
86
92
  else None
87
93
  )
88
94
 
89
95
  if self.settings_fooof["aperiodic"]["offset"]:
90
96
  features_compute[f"{ch_name}_fooof_a_offset"] = (
91
- np.nan_to_num(self.fm.get_params("aperiodic_params", "offset"))
92
- if self.fm.fooofed_spectrum_ is not None
97
+ np.nan_to_num(fm.get_params("aperiodic_params", "offset"))
98
+ if FIT_PASSED is True
93
99
  else None
94
100
  )
95
-
101
+
96
102
  if self.settings_fooof["aperiodic"]["knee"]:
97
- features_compute[f"{ch_name}_fooof_a_knee"] = (
98
- np.nan_to_num(self.fm.get_params("aperiodic_params", "knee"))
99
- if self.fm.fooofed_spectrum_ is not None
100
- else None
103
+ if FIT_PASSED is False:
104
+ knee_freq = None
105
+ else:
106
+ if fm.get_params("aperiodic_params", "exponent") != 0:
107
+ knee_fooof = fm.get_params("aperiodic_params", "knee")
108
+ knee_freq = np.nan_to_num(
109
+ knee_fooof
110
+ ** (
111
+ 1
112
+ / fm.get_params("aperiodic_params", "exponent")
113
+ )
114
+ )
115
+ else:
116
+ knee_freq = None
117
+
118
+ features_compute[f"{ch_name}_fooof_a_knee_frequency"] = (
119
+ knee_freq
101
120
  )
102
121
 
103
122
  peaks_bw = (
104
- self.fm.get_params("peak_params", "BW")
105
- if self.fm.fooofed_spectrum_ is not None
123
+ fm.get_params("peak_params", "BW")
124
+ if FIT_PASSED is True
106
125
  else None
107
126
  )
108
127
  peaks_cf = (
109
- self.fm.get_params("peak_params", "CF")
110
- if self.fm.fooofed_spectrum_ is not None
128
+ fm.get_params("peak_params", "CF")
129
+ if FIT_PASSED is True
111
130
  else None
112
131
  )
113
132
  peaks_pw = (
114
- self.fm.get_params("peak_params", "PW")
115
- if self.fm.fooofed_spectrum_ is not None
133
+ fm.get_params("peak_params", "PW")
134
+ if FIT_PASSED is True
116
135
  else None
117
136
  )
118
137
 
@@ -122,7 +141,6 @@ class FooofAnalyzer(nm_features_abc.Feature):
122
141
  peaks_pw = [peaks_pw]
123
142
 
124
143
  for peak_idx in range(self.max_n_peaks):
125
-
126
144
  if self.settings_fooof["periodic"]["band_width"]:
127
145
  features_compute[f"{ch_name}_fooof_p_{peak_idx}_bw"] = (
128
146
  peaks_bw[peak_idx] if peak_idx < len(peaks_bw) else None
@@ -59,7 +59,7 @@ class MNEConnectivity(nm_features_abc.Feature):
59
59
  if epochs.events.shape[0] < 2:
60
60
  raise Exception(
61
61
  f"A minimum of 2 epochs is required for mne_connectivity,"
62
- f" got only {epochs.events.shape[0]}. Increase settings['segment_length']"
62
+ f" got only {epochs.events.shape[0]}. Increase settings['segment_length_features_ms']"
63
63
  )
64
64
  return epochs
65
65