OSCR

Aperiodicity in Mouse CA1 and DG Power Spectra.

Code ↔ Paper

6 matches between paragraphs of the paper and lines of its authors' code, computed by the harvester (lexical-v1). Click a colored paragraph or line to see its counterpart.

The 6 matches
  1. [1] § Materials and Methods › Code/software › Detection of periodic components ↔ FOOOF codes/fit.py, lines 1679 to 1802 · score 0.77 · FOOOF library, flattened spectrum, original FOOOF, Gaussian fit, subtracting, thresholds · Report a mismatch
  2. [2] § Materials and Methods › Experimental design and statistical analyses › Preprocessing of neural data ↔ FOOOF codes/fit.py, lines 1503 to 1581 · score 0.74 · 43–57 Hz, power spectral density, fitted Gaussian, harmonics, 43 Hz, noise · Report a mismatch
  3. [3] § Materials and Methods › Code/software › Equations for the aperiodic fits ↔ FOOOF codes/funcs.py, lines 151 to 182 · score 0.68 · s1 s2, s3 s2, knee frequencies, exponents, aperiodic, fits · Report a mismatch
  4. [4] § Materials and Methods › Experimental design and statistical analyses › Preprocessing of neural data ↔ Simulations/Figure_1c.m, lines 69 to 175 · score 0.61 · power spectral density, harmonics, nfft, Welch, overlap, window · Report a mismatch
  5. [5] § Materials and Methods › Code/software › Simulated data ↔ FOOOF codes/fit.py, lines 1679 to 1802 · score 0.59 · standard deviation, 2–5 Hz, sgamma, bandwidth, 40 Hz, theta · Report a mismatch
  6. [6] § Materials and Methods › Code/software › Errors estimation ↔ FOOOF codes/fit.py, lines 92 to 163 · score 0.50 · Full model fit, Gaussian fits, periodic components, absolute, aperiodic fit, FOOOF · Report a mismatch

Paper

Loaded from Europe PMC by your browser; OSCR does not store it: doi.org · Europe PMC

The paper is loaded when this pane is shown.

The authors' code

Python · 2,271 lines · 99 KB · no license · 4 matches

  1. """FOOOF Object - base object which defines the model.
  2. Private Attributes
  3. ==================
  4. Private attributes of the FOOOF object are documented here.
  5. Data Attributes
  6. ---------------
  7. _spectrum_flat : 1d array
  8. Flattened power spectrum, with the aperiodic component removed.
  9. _spectrum_peak_rm : 1d array
  10. Power spectrum, with peaks removed.
  11. Model Component Attributes
  12. --------------------------
  13. _ap_fit : 1d array
  14. Values of the isolated aperiodic fit.
  15. _peak_fit : 1d array
  16. Values of the isolated peak fit.
  17. Internal Settings Attributes
  18. ----------------------------
  19. _ap_percentile_thresh : float
  20. Percentile threshold for finding peaks above the aperiodic component.
  21. _ap_guess : list of [float, float, float]
  22. Guess parameters for fitting the aperiodic component.
  23. _ap_bounds : tuple of tuple of float
  24. Upper and lower bounds on fitting aperiodic component.
  25. _cf_bound : float
  26. Parameter bounds for center frequency when fitting gaussians.
  27. _bw_std_edge : float
  28. Bandwidth threshold for edge rejection of peaks, in units of gaussian standard deviation.
  29. _gauss_overlap_thresh : float
  30. Degree of overlap (in units of standard deviation) between gaussian guesses to drop one.
  31. _gauss_std_limits : list of [float, float]
  32. Peak width limits, converted to use for gaussian standard deviation parameter.
  33. This attribute is computed based on `peak_width_limits` and should not be updated directly.
  34. _maxfev : int
  35. The maximum number of calls to the curve fitting function.
  36. _error_metric : str
  37. The error metric to use for post-hoc measures of model fit error.
  38. Run Modes
  39. ---------
  40. _debug : bool
  41. Whether the object is set in debug mode.
  42. This should be controlled by using the `set_debug_mode` method.
  43. _check_data, _check_freqs : bool
  44. Whether to check added inputs for incorrect inputs, failing if present.
  45. Frequency data is checked for linear spacing.
  46. Power values are checked for data for NaN or Inf values.
  47. These modes default to True, and can be controlled with the `set_check_modes` method.
  48. Code Notes
  49. ----------
  50. Methods without defined docstrings import docs at runtime, from aliased external functions.
  51. """
  52. import warnings
  53. from copy import deepcopy
  54. import scipy
  55. import numpy as np
  56. from numpy.linalg import LinAlgError
  57. from scipy.optimize import curve_fit
  58. from scipy.signal import find_peaks
  59. from fooof.core.utils import unlog
  60. from fooof.core.items import OBJ_DESC
  61. from fooof.core.info import get_indices
  62. from fooof.core.io import save_fm, load_json
  63. from fooof.core.reports import save_report_fm
  64. from fooof.core.modutils import copy_doc_func_to_method
  65. from fooof.core.utils import group_three, check_array_dim
  66. from fooof.core.funcs import gaussian_function, get_ap_func, infer_ap_func
  67. from fooof.core.jacobians import jacobian_gauss
  68. from fooof.core.errors import (FitError, NoModelError, DataError,
  69. NoDataError, InconsistentDataError)
  70. from fooof.core.strings import (gen_settings_str, gen_results_fm_str,
  71. gen_issue_str, gen_width_warning_str)
  72. from fooof.utils import interpolate_spectrum
  73. from fooof.plts.fm import plot_fm
  74. from fooof.utils.data import trim_spectrum
  75. from fooof.utils.params import compute_gauss_std
  76. from fooof.data import FOOOFSettings, FOOOFRunModes, FOOOFMetaData, FOOOFResults
  77. from fooof.data.conversions import model_to_dataframe
  78. from fooof.sim.gen import gen_freqs, gen_aperiodic, gen_periodic, gen_model
  79. from statsmodels.tools.eval_measures import aic_sigma , bic_sigma , aicc_sigma , hqic_sigma
  80. import matplotlib.pyplot as plt
  81. ###################################################################################################
  82. ###################################################################################################
  83. class FOOOF():
  84. """Model a physiological power spectrum as a combination of aperiodic and periodic components.
  85. WARNING: FOOOF expects frequency and power values in linear space.
  86. Passing in logged frequencies and/or power spectra is not detected,
  87. and will silently produce incorrect results.
  88. Parameters
  89. ----------
  90. peak_width_limits : tuple of (float, float), optional, default: (0.5, 12.0)
  91. Limits on possible peak width, in Hz, as (lower_bound, upper_bound).
  92. max_n_peaks : int, optional, default: inf
  93. Maximum number of peaks to fit.
  94. min_peak_height : float, optional, default: 0
  95. Absolute threshold for detecting peaks.
  96. This threshold is defined in absolute units of the power spectrum (log power).
  97. peak_threshold : float, optional, default: 2.0
  98. Relative threshold for detecting peaks.
  99. This threshold is defined in relative units of the power spectrum (standard deviation).
  100. aperiodic_mode : {'fixed', 'knee'}
  101. Which approach to take for fitting the aperiodic component.
  102. verbose : bool, optional, default: True
  103. Verbosity mode. If True, prints out warnings and general status updates.
  104. Attributes
  105. ----------
  106. freqs : 1d array
  107. Frequency values for the power spectrum.
  108. power_spectrum : 1d array
  109. Power values, stored internally in log10 scale.
  110. freq_range : list of [float, float]
  111. Frequency range of the power spectrum, as [lowest_freq, highest_freq].
  112. freq_res : float
  113. Frequency resolution of the power spectrum.
  114. fooofed_spectrum_ : 1d array
  115. The full model fit of the power spectrum, in log10 scale.
  116. aperiodic_params_ : 1d array
  117. Parameters that define the aperiodic fit. As [Offset, (Knee), Exponent].
  118. The knee parameter is only included if aperiodic component is fit with a knee.
  119. peak_params_ : 2d array
  120. Fitted parameter values for the peaks. Each row is a peak, as [CF, PW, BW].
  121. gaussian_params_ : 2d array
  122. Parameters that define the gaussian fit(s).
  123. Each row is a gaussian, as [mean, height, standard deviation].
  124. r_squared_ : float
  125. R-squared of the fit between the input power spectrum and the full model fit.
  126. error_ : float
  127. Error of the full model fit.
  128. n_peaks_ : int
  129. The number of peaks fit in the model.
  130. has_data : bool
  131. Whether data is loaded to the object.
  132. has_model : bool
  133. Whether model results are available in the object.
  134. Notes
  135. -----
  136. - Commonly used abbreviations used in this module include:
  137. CF: center frequency, PW: power, BW: Bandwidth, AP: aperiodic
  138. - Input power spectra must be provided in linear scale.
  139. Internally they are stored in log10 scale, as this is what the model operates upon.
  140. - Input power spectra should be smooth, as overly noisy power spectra may lead to bad fits.
  141. For example, raw FFT inputs are not appropriate. Where possible and appropriate, use
  142. longer time segments for power spectrum calculation to get smoother power spectra,
  143. as this will give better model fits.
  144. - The gaussian params are those that define the gaussian of the fit, where as the peak
  145. params are a modified version, in which the CF of the peak is the mean of the gaussian,
  146. the PW of the peak is the height of the gaussian over and above the aperiodic component,
  147. and the BW of the peak, is 2*std of the gaussian (as 'two sided' bandwidth).
  148. """
  149. # pylint: disable=attribute-defined-outside-init
  150. def __init__(self, peak_width_limits=(0.5, 12.0), max_n_peaks=np.inf, min_peak_height=0.01,
  151. peak_threshold=2.0, aperiodic_mode='fixed', verbose=True):
  152. """Initialize object with desired settings."""
  153. # Set input settings
  154. self.peak_width_limits = peak_width_limits
  155. self.max_n_peaks = max_n_peaks
  156. self.min_peak_height = min_peak_height
  157. self.peak_threshold = peak_threshold
  158. self.aperiodic_mode = aperiodic_mode
  159. self.verbose = verbose
  160. ## PRIVATE SETTINGS
  161. # Percentile threshold, to select points from a flat spectrum for an initial aperiodic fit
  162. # Points are selected at a low percentile value to restrict to non-peak points
  163. self._ap_percentile_thresh = 0.025
  164. # Guess parameters for aperiodic fitting, [offset, knee, exponent]
  165. # If offset guess is None, the first value of the power spectrum is used as offset guess
  166. # If exponent guess is None, the abs(log-log slope) of first & last points is used
  167. self._ap_guess = (None, 4, None)
  168. # Bounds for aperiodic fitting, as: ((offset_low_bound, knee_low_bound, exp_low_bound),
  169. # (offset_high_bound, knee_high_bound, exp_high_bound))
  170. # By default, aperiodic fitting is unbound, but can be restricted here, if desired
  171. # Even if fitting without knee, leave bounds for knee (they are dropped later)
  172. # offset,knee_param, exp1, exp2,f = params
  173. self._ap_bounds = ((-np.inf,0,0, 0,0,0), (np.inf,np.inf,np.inf,np.inf,np.inf, np.inf))
  174. # Threshold for how far a peak has to be from edge to keep.
  175. # This is defined in units of gaussian standard deviation
  176. self._bw_std_edge = 1.0
  177. # Degree of overlap between gaussians for one to be dropped
  178. # This is defined in units of gaussian standard deviation
  179. self._gauss_overlap_thresh = 0.7
  180. # Parameter bounds for center frequency when fitting gaussians, in terms of +/- std dev
  181. self._cf_bound = 1.5
  182. # The error metric to calculate, post model fitting. See `_calc_error` for options
  183. # Note: this is for checking error post fitting, not an objective function for fitting
  184. self._error_metric = 'AP'
  185. ## PRIVATE CURVE_FIT SETTINGS
  186. # The maximum number of calls to the curve fitting function
  187. self._maxfev = 5000
  188. # The tolerance setting for curve fitting (see scipy.curve_fit - ftol / xtol / gtol)
  189. # Here reduce tolerance to speed fitting. Set value to 1e-8 to match curve_fit default
  190. self._tol = 0.00001
  191. ## RUN MODES
  192. # Set default debug mode - controls if an error is raised if model fitting is unsuccessful
  193. self._debug = False
  194. # Set default data checking modes - controls which checks get run on input data
  195. # check_freqs: checks the frequency values, and raises an error for uneven spacing
  196. self._check_freqs = True
  197. # check_data: checks the power values and raises an error for any NaN / Inf values
  198. self._check_data = True
  199. # Set internal settings, based on inputs, and initialize data & results attributes
  200. self._reset_internal_settings()
  201. self._reset_data_results(True, True, True)
  202. @property
  203. def has_data(self):
  204. """Indicator for if the object contains data."""
  205. return True if np.any(self.power_spectrum) else False
  206. @property
  207. def has_model(self):
  208. """Indicator for if the object contains a model fit.
  209. Notes
  210. -----
  211. This check uses the aperiodic params, which are:
  212. - nan if no model has been fit
  213. - necessarily defined, as floats, if model has been fit
  214. """
  215. return True if not np.all(np.isnan(self.aperiodic_params_)) else False
  216. @property
  217. def n_peaks_(self):
  218. """How many peaks were fit in the model."""
  219. return self.peak_params_.shape[0] if self.has_model else None
  220. def _reset_internal_settings(self):
  221. """Set, or reset, internal settings, based on what is provided in init.
  222. Notes
  223. -----
  224. These settings are for internal use, based on what is provided to, or set in `__init__`.
  225. They should not be altered by the user.
  226. """
  227. # Only update these settings if other relevant settings are available
  228. if self.peak_width_limits:
  229. # Bandwidth limits are given in 2-sided peak bandwidth
  230. # Convert to gaussian std parameter limits
  231. self._gauss_std_limits = tuple(bwl / 2 for bwl in self.peak_width_limits)
  232. # Otherwise, assume settings are unknown (have been cleared) and set to None
  233. else:
  234. self._gauss_std_limits = None
  235. def _reset_data_results(self, clear_freqs=False, clear_spectrum=False, clear_results=False):
  236. """Set, or reset, data & results attributes to empty.
  237. Parameters
  238. ----------
  239. clear_freqs : bool, optional, default: False
  240. Whether to clear frequency attributes.
  241. clear_spectrum : bool, optional, default: False
  242. Whether to clear power spectrum attribute.
  243. clear_results : bool, optional, default: False
  244. Whether to clear model results attributes.
  245. """
  246. if clear_freqs:
  247. self.freqs = None
  248. self.freq_range = None
  249. self.freq_res = None
  250. if clear_spectrum:
  251. self.power_spectrum = None
  252. if clear_results:
  253. self.aperiodic_params_ = np.array([np.nan] * \
  254. (2 if self.aperiodic_mode == 'fixed' else 3))
  255. self.gaussian_params_ = np.empty([0, 3])
  256. self.peak_params_ = np.empty([0, 3])
  257. self.r_squared_ = np.nan
  258. self.error_ = np.nan
  259. self.fooofed_spectrum_ = None
  260. self._spectrum_flat = None
  261. self._spectrum_peak_rm = None
  262. self._ap_fit = None
  263. self._peak_fit = None
  264. def add_data(self, freqs, power_spectrum, freq_range=None, clear_results=True, knee_fix=None, exp1_fix=None, exp2_fix=None, off_fix=None, off_pr=0.0001, knee_pr=0.0001, exp1_pr=0.0001, exp2_pr=0.0001):
  265. """Add data (frequencies, and power spectrum values) to the current object.
  266. Parameters
  267. ----------
  268. freqs : 1d array
  269. Frequency values for the power spectrum, in linear space.
  270. power_spectrum : 1d array
  271. Power spectrum values, which must be input in linear space.
  272. freq_range : list of [float, float], optional
  273. Frequency range to restrict power spectrum to.
  274. If not provided, keeps the entire range.
  275. clear_results : bool, optional, default: True
  276. Whether to clear prior results, if any are present in the object.
  277. This should only be set to False if data for the current results are being re-added.
  278. Notes
  279. -----
  280. If called on an object with existing data and/or results
  281. they will be cleared by this method call.
  282. """
  283. # If any data is already present, then clear previous data
  284. # Also clear results, if present, unless indicated not to
  285. # This is to ensure object consistency of all data & results
  286. self._reset_data_results(clear_freqs=self.has_data,
  287. clear_spectrum=self.has_data,
  288. clear_results=self.has_model and clear_results)
  289. self.freqs, self.power_spectrum, self.freq_range, self.freq_res = \
  290. self._prepare_data(freqs, power_spectrum, freq_range, 1)
  291. ##added###############
  292. self.off_fix = off_fix
  293. self.knee_fix = knee_fix
  294. self.exp1_fix = exp1_fix
  295. self.exp2_fix = exp2_fix
  296. self.off_pr = off_pr
  297. self.knee_pr = knee_pr
  298. self.exp1_pr = exp1_pr
  299. self.exp2_pr = exp2_pr
  300. ##added###############
  301. def add_settings(self, fooof_settings):
  302. """Add settings into object from a FOOOFSettings object.
  303. Parameters
  304. ----------
  305. fooof_settings : FOOOFSettings
  306. A data object containing the settings for a FOOOF model.
  307. """
  308. for setting in OBJ_DESC['settings']:
  309. setattr(self, setting, getattr(fooof_settings, setting))
  310. self._check_loaded_settings(fooof_settings._asdict())
  311. def add_meta_data(self, fooof_meta_data):
  312. """Add data information into object from a FOOOFMetaData object.
  313. Parameters
  314. ----------
  315. fooof_meta_data : FOOOFMetaData
  316. A meta data object containing meta data information.
  317. """
  318. for meta_dat in OBJ_DESC['meta_data']:
  319. setattr(self, meta_dat, getattr(fooof_meta_data, meta_dat))
  320. self._regenerate_freqs()
  321. def add_results(self, fooof_result):
  322. """Add results data into object from a FOOOFResults object.
  323. Parameters
  324. ----------
  325. fooof_result : FOOOFResults
  326. A data object containing the results from fitting a FOOOF model.
  327. """
  328. self.aperiodic_params_ = fooof_result.aperiodic_params
  329. self.gaussian_params_ = fooof_result.gaussian_params
  330. self.peak_params_ = fooof_result.peak_params
  331. self.r_squared_ = fooof_result.r_squared
  332. self.error_ = fooof_result.error
  333. self._check_loaded_results(fooof_result._asdict())
  334. def report(self, freqs=None, power_spectrum=None, freq_range=None,
  335. plt_log=False, plot_full_range=False, **plot_kwargs):
  336. """Run model fit, and display a report, which includes a plot, and printed results.
  337. Parameters
  338. ----------
  339. freqs : 1d array, optional
  340. Frequency values for the power spectrum.
  341. power_spectrum : 1d array, optional
  342. Power values, which must be input in linear space.
  343. freq_range : list of [float, float], optional
  344. Desired frequency range to fit the model to.
  345. If not provided, fits across the entire given range.
  346. plt_log : bool, optional, default: False
  347. Whether or not to plot the frequency axis in log space.
  348. plot_full_range : bool, default: False
  349. If True, plots the full range of the given power spectrum.
  350. Only relevant / effective if `freqs` and `power_spectrum` passed in in this call.
  351. **plot_kwargs
  352. Keyword arguments to pass into the plot method.
  353. Plot options with a name conflict be passed by pre-pending `plot_`.
  354. e.g. `freqs`, `power_spectrum` and `freq_range`.
  355. Notes
  356. -----
  357. Data is optional, if data has already been added to the object.
  358. """
  359. self.fit(freqs, power_spectrum, freq_range)
  360. self.plot(plt_log=plt_log,
  361. freqs=freqs if plot_full_range else plot_kwargs.pop('plot_freqs', None),
  362. power_spectrum=power_spectrum if \
  363. plot_full_range else plot_kwargs.pop('plot_power_spectrum', None),
  364. freq_range=plot_kwargs.pop('plot_freq_range', None),
  365. **plot_kwargs)
  366. self.print_results(concise=False)
  367. def fit(self, freqs=None, power_spectrum=None, freq_range=None):
  368. """Fit the full power spectrum as a combination of periodic and aperiodic components.
  369. Parameters
  370. ----------
  371. freqs : 1d array, optional
  372. Frequency values for the power spectrum, in linear space.
  373. power_spectrum : 1d array, optional
  374. Power values, which must be input in linear space.
  375. freq_range : list of [float, float], optional
  376. Frequency range to restrict power spectrum to. If not provided, keeps the entire range.
  377. Raises
  378. ------
  379. NoDataError
  380. If no data is available to fit.
  381. FitError
  382. If model fitting fails to fit. Only raised in debug mode.
  383. Notes
  384. -----
  385. Data is optional, if data has already been added to the object.
  386. """
  387. # If freqs & power_spectrum provided together, add data to object.
  388. if freqs is not None and power_spectrum is not None:
  389. self.add_data(freqs, power_spectrum, freq_range)
  390. # If power spectrum provided alone, add to object, and use existing frequency data
  391. # Note: be careful passing in power_spectrum data like this:
  392. # It assumes the power_spectrum is already logged, with correct freq_range
  393. elif isinstance(power_spectrum, np.ndarray):
  394. self.power_spectrum = power_spectrum
  395. # Check that data is available
  396. if not self.has_data:
  397. raise NoDataError("No data available to fit, can not proceed.")
  398. # Check and warn about width limits (if in verbose mode)
  399. if self.verbose:
  400. self._check_width_limits()
  401. # In rare cases, the model fails to fit, and so uses try / except
  402. try:
  403. # If not set to fail on NaN or Inf data at add time, check data here
  404. # This serves as a catch all for curve_fits which will fail given NaN or Inf
  405. # Because FitError's are by default caught, this allows fitting to continue
  406. if not self._check_data:
  407. if np.any(np.isinf(self.power_spectrum)) or np.any(np.isnan(self.power_spectrum)):
  408. raise FitError("Model fitting was skipped because there are NaN or Inf "
  409. "values in the data, which preclude model fitting.")
  410. # Fit the aperiodic component
  411. self.current_ap_fit_params = None #added
  412. self.noise_peaks = None #added
  413. self.aperiodic_params_ = self._robust_ap_fit(self.freqs, self.power_spectrum)
  414. self._ap_fit = gen_aperiodic(self.freqs, self.aperiodic_params_)
  415. # Flatten the power spectrum using fit aperiodic fit
  416. self._spectrum_flat = self.power_spectrum - self._ap_fit
  417. # Find peaks, and fit them with gaussians
  418. self.gaussian_params_ = self._fit_peaks(np.copy(self._spectrum_flat))
  419. # Calculate the peak fit
  420. # Note: if no peaks are found, this creates a flat (all zero) peak fit
  421. self._peak_fit = gen_periodic(self.freqs, np.ndarray.flatten(self.gaussian_params_))
  422. # Create peak-removed (but not flattened) power spectrum
  423. self._spectrum_peak_rm = self.power_spectrum - self._peak_fit
  424. #self.auto_aperiodic(self.freqs, self._spectrum_peak_rm)
  425. # Run final aperiodic fit on peak-removed power spectrum
  426. # This overwrites previous aperiodic fit, and recomputes the flattened spectrum
  427. self.aperiodic_params_ = self._simple_ap_fit(self.freqs, self._spectrum_peak_rm)
  428. self._ap_fit = gen_aperiodic(self.freqs, self.aperiodic_params_)
  429. self._spectrum_flat = self.power_spectrum - self._ap_fit
  430. add_iterations = 20
  431. if np.all(self.gaussian_params_ != [0, 0, 0]):
  432. for it in range(1, add_iterations+1):
  433. self._spectrum_flat = self.power_spectrum - self._ap_fit
  434. if it < add_iterations:
  435. self.gaussian_params_ = self._fit_peaks(np.copy(self._spectrum_flat))
  436. else:
  437. self.gaussian_params_ = np.zeros_like(self.gaussian_params_)
  438. self.gaussian_params_ = self._fit_peaks(np.copy(self._spectrum_flat))
  439. self._peak_fit = gen_periodic(self.freqs, np.ndarray.flatten(self.gaussian_params_))
  440. self._spectrum_peak_rm = self.power_spectrum - self._peak_fit
  441. if it < add_iterations:
  442. self.aperiodic_params_ = self._simple_ap_fit(self.freqs, self._spectrum_peak_rm)
  443. self._ap_fit = gen_aperiodic(self.freqs, self.aperiodic_params_) # either remove this from the last iteration or, reiterate gaussians again
  444. # Create full power_spectrum model fit
  445. self.fooofed_spectrum_ = self._peak_fit + self._ap_fit
  446. # Convert gaussian definitions to peak parameters
  447. self.peak_params_ = self._create_peak_params(self.gaussian_params_)
  448. self.ap_fit = self._ap_fit
  449. # Calculate R^2 and error of the model fit
  450. self._calc_r_squared()
  451. self._calc_error()
  452. except FitError:
  453. # If in debug mode, re-raise the error
  454. if self._debug:
  455. raise
  456. # Clear any interim model results that may have run
  457. # Partial model results shouldn't be interpreted in light of overall failure
  458. self._reset_data_results(clear_results=True)
  459. # Print out status
  460. if self.verbose:
  461. print("Model fitting was unsuccessful.")
  462. def print_settings(self, description=False, concise=False):
  463. """Print out the current settings.
  464. Parameters
  465. ----------
  466. description : bool, optional, default: False
  467. Whether to print out a description with current settings.
  468. concise : bool, optional, default: False
  469. Whether to print the report in a concise mode, or not.
  470. """
  471. print(gen_settings_str(self, description, concise))
  472. def print_results(self, concise=False):
  473. """Print out model fitting results.
  474. Parameters
  475. ----------
  476. concise : bool, optional, default: False
  477. Whether to print the report in a concise mode, or not.
  478. """
  479. print(gen_results_fm_str(self, concise))
  480. @staticmethod
  481. def print_report_issue(concise=False):
  482. """Prints instructions on how to report bugs and/or problematic fits.
  483. Parameters
  484. ----------
  485. concise : bool, optional, default: False
  486. Whether to print the report in a concise mode, or not.
  487. """
  488. print(gen_issue_str(concise))
  489. def get_settings(self):
  490. """Return user defined settings of the current object.
  491. Returns
  492. -------
  493. FOOOFSettings
  494. Object containing the settings from the current object.
  495. """
  496. return FOOOFSettings(**{key : getattr(self, key) \
  497. for key in OBJ_DESC['settings']})
  498. def get_run_modes(self):
  499. """Return run modes of the current object.
  500. Returns
  501. -------
  502. FOOOFRunModes
  503. Object containing the run modes from the current object.
  504. """
  505. return FOOOFRunModes(**{key.strip('_') : getattr(self, key) \
  506. for key in OBJ_DESC['run_modes']})
  507. def get_meta_data(self):
  508. """Return data information from the current object.
  509. Returns
  510. -------
  511. FOOOFMetaData
  512. Object containing meta data from the current object.
  513. """
  514. return FOOOFMetaData(**{key : getattr(self, key) \
  515. for key in OBJ_DESC['meta_data']})
  516. def get_data(self, component='full', space='log'):
  517. """Get a data component.
  518. Parameters
  519. ----------
  520. component : {'full', 'aperiodic', 'peak'}
  521. Which data component to return.
  522. 'full' - full power spectrum
  523. 'aperiodic' - isolated aperiodic data component
  524. 'peak' - isolated peak data component
  525. space : {'log', 'linear'}
  526. Which space to return the data component in.
  527. 'log' - returns in log10 space.
  528. 'linear' - returns in linear space.
  529. Returns
  530. -------
  531. output : 1d array
  532. Specified data component, in specified spacing.
  533. Notes
  534. -----
  535. The 'space' parameter doesn't just define the spacing of the data component
  536. values, but rather defines the space of the additive data definition such that
  537. `power_spectrum = aperiodic_component + peak_component`.
  538. With space set as 'log', this combination holds in log space.
  539. With space set as 'linear', this combination holds in linear space.
  540. """
  541. if not self.has_data:
  542. raise NoDataError("No data available to fit, can not proceed.")
  543. assert space in ['linear', 'log'], "Input for 'space' invalid."
  544. if component == 'full':
  545. output = self.power_spectrum if space == 'log' else unlog(self.power_spectrum)
  546. elif component == 'aperiodic':
  547. output = self._spectrum_peak_rm if space == 'log' else \
  548. unlog(self.power_spectrum) / unlog(self._peak_fit)
  549. elif component == 'peak':
  550. output = self._spectrum_flat if space == 'log' else \
  551. unlog(self.power_spectrum) - unlog(self._ap_fit)
  552. else:
  553. raise ValueError('Input for component invalid.')
  554. return output
  555. def get_model(self, component='full', space='log'):
  556. """Get a model component.
  557. Parameters
  558. ----------
  559. component : {'full', 'aperiodic', 'peak'}
  560. Which model component to return.
  561. 'full' - full model
  562. 'aperiodic' - isolated aperiodic model component
  563. 'peak' - isolated peak model component
  564. space : {'log', 'linear'}
  565. Which space to return the model component in.
  566. 'log' - returns in log10 space.
  567. 'linear' - returns in linear space.
  568. Returns
  569. -------
  570. output : 1d array
  571. Specified model component, in specified spacing.
  572. Notes
  573. -----
  574. The 'space' parameter doesn't just define the spacing of the model component
  575. values, but rather defines the space of the additive model such that
  576. `model = aperiodic_component + peak_component`.
  577. With space set as 'log', this combination holds in log space.
  578. With space set as 'linear', this combination holds in linear space.
  579. """
  580. if not self.has_model:
  581. raise NoModelError("No model fit results are available, can not proceed.")
  582. assert space in ['linear', 'log'], "Input for 'space' invalid."
  583. if component == 'full':
  584. output = self.fooofed_spectrum_ if space == 'log' else unlog(self.fooofed_spectrum_)
  585. elif component == 'aperiodic':
  586. output = self._ap_fit if space == 'log' else unlog(self._ap_fit)
  587. elif component == 'peak':
  588. output = self._peak_fit if space == 'log' else \
  589. unlog(self.fooofed_spectrum_) - unlog(self._ap_fit)
  590. else:
  591. raise ValueError('Input for component invalid.')
  592. return output
  593. def get_params(self, name, col=None):
  594. """Return model fit parameters for specified feature(s).
  595. Parameters
  596. ----------
  597. name : {'aperiodic_params', 'peak_params', 'gaussian_params', 'error', 'r_squared'}
  598. Name of the data field to extract.
  599. col : {'CF', 'PW', 'BW', 'offset', 'knee', 'exponent'} or int, optional
  600. Column name / index to extract from selected data, if requested.
  601. Only used for name of {'aperiodic_params', 'peak_params', 'gaussian_params'}.
  602. Returns
  603. -------
  604. out : float or 1d array
  605. Requested data.
  606. Raises
  607. ------
  608. NoModelError
  609. If there are no model fit parameters available to return.
  610. Notes
  611. -----
  612. If there are no fit peak (no peak parameters), this method will return NaN.
  613. """
  614. if not self.has_model:
  615. raise NoModelError("No model fit results are available to extract, can not proceed.")
  616. # If col specified as string, get mapping back to integer
  617. if isinstance(col, str):
  618. col = get_indices(self.aperiodic_mode)[col]
  619. # Allow for shortcut alias, without adding `_params`
  620. if name in ['aperiodic', 'peak', 'gaussian']:
  621. name = name + '_params'
  622. # Extract the request data field from object
  623. out = getattr(self, name + '_')
  624. # Periodic values can be empty arrays and if so, replace with NaN array
  625. if isinstance(out, np.ndarray) and out.size == 0:
  626. out = np.array([np.nan, np.nan, np.nan])
  627. # Select out a specific column, if requested
  628. if col is not None:
  629. # Extract column, & if result is a single value in an array, unpack from array
  630. out = out[col] if out.ndim == 1 else out[:, col]
  631. out = out[0] if isinstance(out, np.ndarray) and out.size == 1 else out
  632. return out
  633. def get_results(self):
  634. """Return model fit parameters and goodness of fit metrics.
  635. Returns
  636. -------
  637. FOOOFResults
  638. Object containing the model fit results from the current object.
  639. """
  640. return FOOOFResults(**{key.strip('_') : getattr(self, key) \
  641. for key in OBJ_DESC['results']})
  642. @copy_doc_func_to_method(plot_fm)
  643. def plot(self, plot_peaks=None, plot_aperiodic=True, freqs=None, power_spectrum=None,
  644. freq_range=None, plt_log=False, add_legend=True, save_fig=False, file_name=None,
  645. file_path=None, ax=None, data_kwargs=None, model_kwargs=None,
  646. aperiodic_kwargs=None, peak_kwargs=None, **plot_kwargs):
  647. plot_fm(self, plot_peaks=plot_peaks, plot_aperiodic=plot_aperiodic, freqs=freqs,
  648. power_spectrum=power_spectrum, freq_range=freq_range, plt_log=plt_log,
  649. add_legend=add_legend, save_fig=save_fig, file_name=file_name,
  650. file_path=file_path, ax=ax, data_kwargs=data_kwargs, model_kwargs=model_kwargs,
  651. aperiodic_kwargs=aperiodic_kwargs, peak_kwargs=peak_kwargs, **plot_kwargs)
  652. @copy_doc_func_to_method(save_report_fm)
  653. def save_report(self, file_name, file_path=None, plt_log=False,
  654. add_settings=True, **plot_kwargs):
  655. save_report_fm(self, file_name, file_path, plt_log, add_settings, **plot_kwargs)
  656. @copy_doc_func_to_method(save_fm)
  657. def save(self, file_name, file_path=None, append=False,
  658. save_results=False, save_settings=False, save_data=False):
  659. save_fm(self, file_name, file_path, append, save_results, save_settings, save_data)
  660. def load(self, file_name, file_path=None, regenerate=True):
  661. """Load in a FOOOF formatted JSON file to the current object.
  662. Parameters
  663. ----------
  664. file_name : str or FileObject
  665. File to load data from.
  666. file_path : Path or str, optional
  667. Path to directory to load from. If None, loads from current directory.
  668. regenerate : bool, optional, default: True
  669. Whether to regenerate the model fit from the loaded data, if data is available.
  670. """
  671. # Reset data in object, so old data can't interfere
  672. self._reset_data_results(True, True, True)
  673. # Load JSON file, add to self and check loaded data
  674. data = load_json(file_name, file_path)
  675. self._add_from_dict(data)
  676. self._check_loaded_settings(data)
  677. self._check_loaded_results(data)
  678. # Regenerate model components, based on what is available
  679. if regenerate:
  680. if self.freq_res:
  681. self._regenerate_freqs()
  682. if np.all(self.freqs) and np.all(self.aperiodic_params_):
  683. self._regenerate_model()
  684. def copy(self):
  685. """Return a copy of the current object."""
  686. return deepcopy(self)
  687. def set_debug_mode(self, debug):
  688. """Set debug mode, which controls if an error is raised if model fitting is unsuccessful.
  689. Parameters
  690. ----------
  691. debug : bool
  692. Whether to run in debug mode.
  693. """
  694. self._debug = debug
  695. def set_check_modes(self, check_freqs=None, check_data=None):
  696. """Set check modes, which controls if an error is raised based on check on the inputs.
  697. Parameters
  698. ----------
  699. check_freqs : bool, optional
  700. Whether to run in check freqs mode, which checks the frequency data.
  701. check_data : bool, optional
  702. Whether to run in check data mode, which checks the power spectrum values data.
  703. """
  704. if check_freqs is not None:
  705. self._check_freqs = check_freqs
  706. if check_data is not None:
  707. self._check_data = check_data
  708. # This kept for backwards compatibility, but to be removed in 2.0 in favor of `set_check_modes`
  709. def set_check_data_mode(self, check_data):
  710. """Set check data mode, which controls if an error is raised if NaN or Inf data are added.
  711. Parameters
  712. ----------
  713. check_data : bool
  714. Whether to run in check data mode.
  715. """
  716. self.set_check_modes(check_data=check_data)
  717. def set_run_modes(self, debug, check_freqs, check_data):
  718. """Simultaneously set all run modes.
  719. Parameters
  720. ----------
  721. debug : bool
  722. Whether to run in debug mode.
  723. check_freqs : bool
  724. Whether to run in check freqs mode.
  725. check_data : bool
  726. Whether to run in check data mode.
  727. """
  728. self.set_debug_mode(debug)
  729. self.set_check_modes(check_freqs, check_data)
  730. def to_df(self, peak_org):
  731. """Convert and extract the model results as a pandas object.
  732. Parameters
  733. ----------
  734. peak_org : int or Bands
  735. How to organize peaks.
  736. If int, extracts the first n peaks.
  737. If Bands, extracts peaks based on band definitions.
  738. Returns
  739. -------
  740. pd.Series
  741. Model results organized into a pandas object.
  742. """
  743. return model_to_dataframe(self.get_results(), peak_org)
  744. def _check_width_limits(self):
  745. """Check and warn about peak width limits / frequency resolution interaction."""
  746. # Check peak width limits against frequency resolution and warn if too close
  747. if 1.5 * self.freq_res >= self.peak_width_limits[0]:
  748. print(gen_width_warning_str(self.freq_res, self.peak_width_limits[0]))
  749. def _simple_ap_fit(self, freqs, power_spectrum):
  750. """
  751. Fit the aperiodic component of the power spectrum.
  752. Parameters
  753. ----------
  754. freqs : 1d array
  755. Frequency values for the power_spectrum, in linear scale.
  756. power_spectrum : 1d array
  757. Power values, in log10 scale.
  758. Returns
  759. -------
  760. aperiodic_params : 1d array
  761. Parameter estimates for aperiodic fit.
  762. """
  763. #guess parameters for 1exp and flat+1exp
  764. off_guess = [power_spectrum[0] if not self._ap_guess[0] else self._ap_guess[0]]
  765. kne_guess = [self._ap_guess[1]] if self.aperiodic_mode == 'flat_1exp' else []
  766. exp_guess = [np.abs((self.power_spectrum[-1] - self.power_spectrum[0]) /
  767. (np.log10(self.freqs[-1]) - np.log10(self.freqs[0])))
  768. if not self._ap_guess[2] else self._ap_guess[2]]
  769. if self.aperiodic_mode == 'fixed' or self.aperiodic_mode == 'flat_1exp':
  770. if self.aperiodic_mode == 'flat_1exp':
  771. ap_bounds = ((-np.inf, self.freq_range[0], 0), (np.inf, self.freq_range[1], np.inf))
  772. elif self.aperiodic_mode == 'fixed':
  773. ap_bounds = tuple(bound[0:2] for bound in self._ap_bounds)
  774. # Collect together guess parameters
  775. guess = np.array(off_guess + kne_guess + exp_guess)
  776. if self.current_ap_fit_params is not None:
  777. guess = self.current_ap_fit_params
  778. try:
  779. with warnings.catch_warnings():
  780. warnings.simplefilter("ignore")
  781. aperiodic_params, _ = curve_fit(get_ap_func(self.aperiodic_mode),
  782. freqs, power_spectrum, p0=guess,
  783. maxfev=self._maxfev, bounds=ap_bounds,
  784. ftol=self._tol, xtol=self._tol, gtol=self._tol,
  785. check_finite=False)
  786. except RuntimeError as excp:
  787. error_msg = "Model fitting failed due to not finding parameters in the simple aperiodic component fit."
  788. raise FitError(error_msg) from excp
  789. elif self.aperiodic_mode == '2exp':
  790. # Select subregion of PSD
  791. f_mask = np.logical_and(freqs >= self.freq_range[0], freqs <= self.freq_range[1])
  792. freqs = freqs[f_mask]
  793. power_spectrum = power_spectrum[f_mask]
  794. # Calculate guess parameters for the 1st exponent
  795. freq_res = freqs[1] - freqs[0]
  796. start_index1 = round(13 / freq_res)
  797. end_index1 = round(45 / freq_res)
  798. f1 = freqs[start_index1:end_index1]
  799. u1 = np.log10(f1)
  800. p1 = power_spectrum[start_index1:end_index1]
  801. guess_exp1 = [(p1[-1] - p1[0]) / (u1[0] - u1[-1])]
  802. off1 = (guess_exp1[0] * u1[0]) + p1[0]
  803. # Calculate guess parameters for the 2nd exponent, knee, and power at knee
  804. start_index2 = round(75 / freq_res)
  805. end_index2 = round(120/ freq_res)
  806. f2 = freqs[start_index2:end_index2]
  807. u2 = np.log10(f2)
  808. p2 = power_spectrum[start_index2:end_index2]
  809. guess_exp2 = [(p2[-1] - p2[0]) / (u2[0] - u2[-1])]
  810. off2 = (guess_exp2[0] * u2[0] + p2[0])
  811. knee_guess = [((off2 - off1) / (guess_exp2[0] - guess_exp1[0]))]
  812. knee_off_guess = [np.log10(2) + (off1 - (guess_exp1[0] * knee_guess[0]))]
  813. knee_guess = [10**(knee_guess[0])]
  814. guess = np.array(knee_off_guess + knee_guess + guess_exp1 + guess_exp2)
  815. ap_bounds = ((-np.inf, self.freq_range[0], 0, 0), (np.inf, self.freq_range[1], np.inf, np.inf))
  816. #avoid error, if guess is outside of ap_bounds #added
  817. for g in range(0, len(guess)):
  818. if guess[g] < ap_bounds[0][g]:
  819. guess[g] = ap_bounds[0][g]
  820. elif guess[g] > ap_bounds[1][g]:
  821. guess[g] = ap_bounds[1][g]
  822. if self.current_ap_fit_params is not None: #added
  823. guess = self.current_ap_fit_params #added
  824. ######added optional fixation of ap parameters ##############################
  825. if self.knee_fix is not None:
  826. guess[1] = self.knee_fix
  827. lower_bounds_l = list(ap_bounds[0])
  828. upper_bounds_l = list(ap_bounds[1])
  829. lower_bounds_l[1] = self.knee_fix - self.knee_pr
  830. if lower_bounds_l[1] < self.freq_range[0]:
  831. lower_bounds_l[1] = self.freq_range[0]
  832. upper_bounds_l[1] = self.knee_fix + self.knee_pr
  833. upper_bounds_l[1] = self.knee_fix + self.knee_pr
  834. ap_bounds = (tuple(lower_bounds_l), tuple(upper_bounds_l))
  835. if self.exp1_fix is not None:
  836. guess[2] = self.exp1_fix
  837. lower_bounds_l = list(ap_bounds[0])
  838. upper_bounds_l = list(ap_bounds[1])
  839. lower_bounds_l[2] = self.exp1_fix - self.exp1_pr
  840. upper_bounds_l[2] = self.exp1_fix + self.exp1_pr
  841. ap_bounds = (tuple(lower_bounds_l), tuple(upper_bounds_l))
  842. if self.exp2_fix is not None:
  843. guess[3] = self.exp2_fix
  844. lower_bounds_l = list(ap_bounds[0])
  845. upper_bounds_l = list(ap_bounds[1])
  846. lower_bounds_l[3] = self.exp2_fix - self.exp2_pr
  847. upper_bounds_l[3] = self.exp2_fix + self.exp2_pr
  848. ap_bounds = (tuple(lower_bounds_l), tuple(upper_bounds_l))
  849. if self.off_fix is not None:
  850. guess[0] = self.off_fix
  851. lower_bounds_l = list(ap_bounds[0])
  852. upper_bounds_l = list(ap_bounds[1])
  853. lower_bounds_l[0] = self.off_fix - self.off_pr
  854. upper_bounds_l[0] = self.off_fix + self.off_pr
  855. ap_bounds = (tuple(lower_bounds_l), tuple(upper_bounds_l))
  856. ######added optional fixation of ap parameters ##############################
  857. try:
  858. with warnings.catch_warnings():
  859. warnings.simplefilter("ignore")
  860. aperiodic_params, _ = curve_fit(get_ap_func(self.aperiodic_mode),
  861. freqs, power_spectrum, p0=guess,
  862. maxfev=self._maxfev, bounds=ap_bounds,
  863. ftol=self._tol, xtol=self._tol, gtol=self._tol,
  864. check_finite=False)
  865. except RuntimeError as excp:
  866. error_msg = ("Model fitting failed due to not finding parameters in "
  867. "the simple aperiodic component fit.")
  868. raise FitError(error_msg) from excp
  869. if aperiodic_params[2] > aperiodic_params[3]:
  870. exp1_copy = aperiodic_params[2].copy()
  871. exp2_copy = aperiodic_params[3].copy()
  872. aperiodic_params[2] = exp2_copy
  873. aperiodic_params[3] = exp1_copy
  874. elif self.aperiodic_mode == "2exp_flat":
  875. f_mask = np.logical_and(freqs >= self.freq_range[0], freqs <= self.freq_range[1])
  876. freqs = freqs[f_mask]
  877. power_spectrum = power_spectrum[f_mask]
  878. # Calculate guess parameters for exp1
  879. freq_res = freqs[1] - freqs[0]
  880. start_index1 = round(13 / freq_res)
  881. end_index1 = round(45 / freq_res)
  882. f1 = freqs[start_index1:end_index1]
  883. u1 = np.log10(f1)
  884. p1 = power_spectrum[start_index1:end_index1]
  885. guess_exp1 = [(p1[-1] - p1[0]) / (u1[0] - u1[-1])]
  886. off1 = (guess_exp1[0] * u1[0]) + p1[0]
  887. # Calculate guess parameters for exp2 and knee
  888. start_index2 = round(75 / freq_res)
  889. end_index2 = round(180 / freq_res)
  890. f2 = freqs[start_index2:end_index2]
  891. u2 = np.log10(f2)
  892. p2 = power_spectrum[start_index2:end_index2]
  893. guess_exp2 = [(p2[-1] - p2[0]) / (u2[0] - u2[-1])]
  894. new_guess_exp2 = [np.abs(guess_exp2[0])]
  895. off2 = (guess_exp2[0] * u2[0] + p2[0])
  896. knee_guess = [(off2 - off1) / (guess_exp2[0] - guess_exp1[0])]
  897. # Calculate guess parameters for flat_freq, power at knee frequency
  898. start_index3 = round(300 / freq_res)
  899. end_index3 = round(495/ freq_res)
  900. f3 = freqs[start_index3:end_index3]
  901. u3 = np.log10(f3)
  902. p3 = power_spectrum[start_index3:end_index3]
  903. flat_off_guess = [np.mean(p3)]
  904. flat_freq_guess = [(flat_off_guess[0] - off2)/(-1*new_guess_exp2[0])]
  905. knee_off_guess = [np.log10(2) + (off1 - (guess_exp1[0] * knee_guess[0]))]
  906. guess = np.array(knee_off_guess + knee_guess + flat_freq_guess + guess_exp1 + new_guess_exp2)
  907. ap_bounds = ((-np.inf,self.freq_range[0], self.freq_range[0], 0 , 0), (np.inf, self.freq_range[1], self.freq_range[1], np.inf , np.inf))
  908. if self.freq_range[1] < 400:
  909. flat_freq_guess = [300]
  910. guess = np.array(knee_off_guess + knee_guess + flat_freq_guess + guess_exp1 + new_guess_exp2)
  911. ap_bounds = ((-np.inf,self.freq_range[0], self.freq_range[0], 0 , 0), (np.inf, 490, 490, np.inf , np.inf))
  912. #avoid error, if guess is outside of ap_bounds #added
  913. for g in range(0, len(guess)):
  914. if guess[g] < ap_bounds[0][g]:
  915. guess[g] = ap_bounds[0][g]
  916. elif guess[g] > ap_bounds[1][g]:
  917. guess[g] = ap_bounds[1][g]
  918. if self.current_ap_fit_params is not None: #added
  919. guess = self.current_ap_fit_params #added
  920. ######added optional fixation of ap parameters ##############################
  921. if self.knee_fix is not None:
  922. guess[1] = self.knee_fix
  923. lower_bounds_l = list(ap_bounds[0])
  924. upper_bounds_l = list(ap_bounds[1])
  925. lower_bounds_l[1] = self.knee_fix - self.knee_pr
  926. if lower_bounds_l[1] < self.freq_range[0]:
  927. lower_bounds_l[1] = self.freq_range[0]
  928. upper_bounds_l[1] = self.knee_fix + self.knee_pr
  929. upper_bounds_l[1] = self.knee_fix + self.knee_pr
  930. ap_bounds = (tuple(lower_bounds_l), tuple(upper_bounds_l))
  931. if self.exp1_fix is not None:
  932. guess[3] = self.exp1_fix
  933. lower_bounds_l = list(ap_bounds[0])
  934. upper_bounds_l = list(ap_bounds[1])
  935. lower_bounds_l[3] = self.exp1_fix - self.exp1_pr
  936. upper_bounds_l[3] = self.exp1_fix + self.exp1_pr
  937. ap_bounds = (tuple(lower_bounds_l), tuple(upper_bounds_l))
  938. if self.exp2_fix is not None:
  939. guess[4] = self.exp2_fix
  940. lower_bounds_l = list(ap_bounds[0])
  941. upper_bounds_l = list(ap_bounds[1])
  942. lower_bounds_l[4] = self.exp2_fix - self.exp2_pr
  943. upper_bounds_l[4] = self.exp2_fix + self.exp2_pr
  944. ap_bounds = (tuple(lower_bounds_l), tuple(upper_bounds_l))
  945. if self.off_fix is not None:
  946. guess[0] = self.off_fix
  947. lower_bounds_l = list(ap_bounds[0])
  948. upper_bounds_l = list(ap_bounds[1])
  949. lower_bounds_l[0] = self.off_fix - self.off_pr
  950. upper_bounds_l[0] = self.off_fix + self.off_pr
  951. ap_bounds = (tuple(lower_bounds_l), tuple(upper_bounds_l))
  952. ######added optional fixation of ap parameters ##############################
  953. if guess[2] < guess[1]:
  954. guess[2] = guess[1] + 30
  955. if guess[2] > self.freq_range[1] and self.freq_range[1] > 400:
  956. guess[2] = 0.8*self.freq_range[1]
  957. aperiodic_params, _ = curve_fit(
  958. get_ap_func(self.aperiodic_mode),
  959. freqs, power_spectrum, p0=guess,
  960. maxfev=self._maxfev, bounds=ap_bounds,
  961. ftol=self._tol, xtol=self._tol, gtol=self._tol,
  962. check_finite=False
  963. )
  964. if aperiodic_params[1] > aperiodic_params[2]:
  965. knee_copy = aperiodic_params[1].copy()
  966. flat_freq_copy = aperiodic_params[2].copy()
  967. aperiodic_params[1] = flat_freq_copy
  968. aperiodic_params[2] = knee_copy
  969. elif self.aperiodic_mode == "3exp":
  970. f_mask = np.logical_and(freqs >= self.freq_range[0], freqs <= self.freq_range[1])
  971. freqs = freqs[f_mask]
  972. power_spectrum = power_spectrum[f_mask]
  973. original_psd = power_spectrum.copy() # vis
  974. # Calculate guess parameters for exp1
  975. freq_res = freqs[1] - freqs[0]
  976. start_index1 = round(13 / freq_res)
  977. end_index1 = round(45 / freq_res)
  978. f1 = freqs[start_index1:end_index1]
  979. u1 = np.log10(f1)
  980. p1 = power_spectrum[start_index1:end_index1]
  981. guess_exp1 = [(p1[-1] - p1[0]) / (u1[0] - u1[-1])]
  982. off1 = (guess_exp1[0] * u1[0]) + p1[0]
  983. # Calculate guess parameters for exp2, exp3, knee, flat_freq, power at knee frequency for fitting untill 500 Hz
  984. start_index2 = round(75 / freq_res)
  985. end_index2 = round(180 / freq_res)
  986. f2 = freqs[start_index2:end_index2]
  987. u2 = np.log10(f2)
  988. p2 = power_spectrum[start_index2:end_index2]
  989. guess_exp2 = [(p2[-1] - p2[0]) / (u2[0] - u2[-1])]
  990. new_guess_exp2 = [np.abs(guess_exp2[0])]
  991. off2 = (guess_exp2[0] * u2[0] + p2[0])
  992. knee_guess = [(off2 - off1) / (guess_exp2[0] - guess_exp1[0])]
  993. start_index3 = round(300 / freq_res)
  994. end_index3 = round(495/ freq_res)
  995. f3 = freqs[start_index3:end_index3]
  996. u3 = np.log10(f3)
  997. p3 = power_spectrum[start_index3:end_index3]
  998. flat_off_guess = [np.mean(p3)]
  999. flat_freq_guess = [(flat_off_guess[0] - off2)/(-1*new_guess_exp2[0])]
  1000. exp3_guess = [0]
  1001. knee_off_guess = [np.log10(2) + (off1 - (guess_exp1[0] * knee_guess[0]))]
  1002. #flat_fr_logged_guess = [2.3] # Adjust this initial guess if needed
  1003. guess = np.array(knee_off_guess + knee_guess + flat_freq_guess + guess_exp1 + new_guess_exp2 + exp3_guess)
  1004. ap_bounds = ((-np.inf,self.freq_range[0], self.freq_range[0], 0 , 0, 0), (np.inf, self.freq_range[1], self.freq_range[1], np.inf , np.inf, np.inf))
  1005. #guess flat_freq if fitting range is <400 Hz
  1006. if self.freq_range[1] < 400:
  1007. flat_freq_guess = [300]
  1008. guess = np.array(knee_off_guess + knee_guess + flat_freq_guess + guess_exp1 + new_guess_exp2 + exp3_guess)
  1009. ap_bounds = ((-np.inf,self.freq_range[0], self.freq_range[0], 0 , 0, 0), (np.inf, 490, 490, np.inf , np.inf, np.inf))
  1010. #avoid error, if guess is outside of ap_bounds #added
  1011. for g in range(0, len(guess)):
  1012. if guess[g] < ap_bounds[0][g]:
  1013. guess[g] = ap_bounds[0][g]
  1014. elif guess[g] > ap_bounds[1][g]:
  1015. guess[g] = ap_bounds[1][g]
  1016. if self.current_ap_fit_params is not None: #added
  1017. guess = self.current_ap_fit_params #added
  1018. ######added optional fixation of ap parameters ##############################
  1019. if self.knee_fix is not None:
  1020. guess[1] = self.knee_fix
  1021. lower_bounds_l = list(ap_bounds[0])
  1022. upper_bounds_l = list(ap_bounds[1])
  1023. lower_bounds_l[1] = self.knee_fix - self.knee_pr
  1024. if lower_bounds_l[1] < self.freq_range[0]:
  1025. lower_bounds_l[1] = self.freq_range[0]
  1026. upper_bounds_l[1] = self.knee_fix + self.knee_pr
  1027. upper_bounds_l[1] = self.knee_fix + self.knee_pr
  1028. ap_bounds = (tuple(lower_bounds_l), tuple(upper_bounds_l))
  1029. if self.exp1_fix is not None:
  1030. guess[3] = self.exp1_fix
  1031. lower_bounds_l = list(ap_bounds[0])
  1032. upper_bounds_l = list(ap_bounds[1])
  1033. lower_bounds_l[3] = self.exp1_fix - self.exp1_pr
  1034. upper_bounds_l[3] = self.exp1_fix + self.exp1_pr
  1035. ap_bounds = (tuple(lower_bounds_l), tuple(upper_bounds_l))
  1036. if self.exp2_fix is not None:
  1037. guess[4] = self.exp2_fix
  1038. lower_bounds_l = list(ap_bounds[0])
  1039. upper_bounds_l = list(ap_bounds[1])
  1040. lower_bounds_l[4] = self.exp2_fix - self.exp2_pr
  1041. upper_bounds_l[4] = self.exp2_fix + self.exp2_pr
  1042. ap_bounds = (tuple(lower_bounds_l), tuple(upper_bounds_l))
  1043. if self.off_fix is not None:
  1044. guess[0] = self.off_fix
  1045. lower_bounds_l = list(ap_bounds[0])
  1046. upper_bounds_l = list(ap_bounds[1])
  1047. lower_bounds_l[0] = self.off_fix - self.off_pr
  1048. upper_bounds_l[0] = self.off_fix + self.off_pr
  1049. ap_bounds = (tuple(lower_bounds_l), tuple(upper_bounds_l))
  1050. ######added optional fixation of ap parameters ##############################
  1051. if guess[2] < guess[1]:
  1052. guess[2] = guess[1] + 30
  1053. if guess[2] > self.freq_range[1] and self.freq_range[1] > 400:
  1054. guess[2] = 0.8*self.freq_range[1]
  1055. aperiodic_params, _ = curve_fit(
  1056. get_ap_func(self.aperiodic_mode),
  1057. freqs, power_spectrum, p0=guess,
  1058. maxfev=self._maxfev, bounds=ap_bounds,
  1059. ftol=self._tol, xtol=self._tol, gtol=self._tol,
  1060. check_finite=False
  1061. )
  1062. if aperiodic_params[1] > aperiodic_params[2]:
  1063. knee_copy = aperiodic_params[1].copy()
  1064. flat_freq_copy = aperiodic_params[2].copy()
  1065. aperiodic_params[1] = flat_freq_copy
  1066. aperiodic_params[2] = knee_copy
  1067. self.current_ap_fit_params = aperiodic_params #added
  1068. return aperiodic_params
  1069. def _robust_ap_fit(self, freqs, power_spectrum):
  1070. """Fit the aperiodic component of the power spectrum robustly, ignoring outliers.
  1071. Parameters
  1072. ----------
  1073. freqs : 1d array
  1074. Frequency values for the power spectrum, in linear scale.
  1075. power_spectrum : 1d array
  1076. Power values, in log10 scale.
  1077. Returns
  1078. -------
  1079. aperiodic_params : 1d array
  1080. Parameter estimates for aperiodic fit.
  1081. Raises
  1082. ------
  1083. FitError
  1084. If the fitting encounters an error.
  1085. """
  1086. # Do a quick, initial aperiodic fit
  1087. popt = self._simple_ap_fit(freqs, power_spectrum)
  1088. initial_fit = gen_aperiodic(freqs, popt)
  1089. # Flatten power_spectrum based on initial aperiodic fit
  1090. flatspec = power_spectrum - initial_fit
  1091. # Flatten outliers, defined as any points that drop below 0
  1092. flatspec[flatspec < 0] = 0
  1093. # Use percentile threshold, in terms of # of points, to extract and re-fit
  1094. perc_thresh = np.percentile(flatspec, self._ap_percentile_thresh)
  1095. perc_mask = flatspec <= perc_thresh
  1096. freqs_ignore = freqs[perc_mask]
  1097. spectrum_ignore = power_spectrum[perc_mask]
  1098. # Get bounds for aperiodic fitting, dropping knee bound if not set to fit knee
  1099. if self.aperiodic_mode == '2exp':
  1100. ap_bounds = ((-np.inf, self.freq_range[0], 0, 0), (np.inf, self.freq_range[1], np.inf, np.inf))
  1101. ######added optional fixation of ap parameters ##############################
  1102. if self.knee_fix is not None:
  1103. popt[1] = self.knee_fix
  1104. lower_bounds_l = list(ap_bounds[0])
  1105. upper_bounds_l = list(ap_bounds[1])
  1106. lower_bounds_l[1] = self.knee_fix - self.knee_pr
  1107. if lower_bounds_l[1] < self.freq_range[0]:
  1108. lower_bounds_l[1] = self.freq_range[0]
  1109. upper_bounds_l[1] = self.knee_fix + self.knee_pr
  1110. ap_bounds = (tuple(lower_bounds_l), tuple(upper_bounds_l))
  1111. if self.exp1_fix is not None:
  1112. popt[2] = self.exp1_fix
  1113. lower_bounds_l = list(ap_bounds[0])
  1114. upper_bounds_l = list(ap_bounds[1])
  1115. lower_bounds_l[2] = self.exp1_fix - self.exp1_pr
  1116. upper_bounds_l[2] = self.exp1_fix + self.exp1_pr
  1117. ap_bounds = (tuple(lower_bounds_l), tuple(upper_bounds_l))
  1118. if self.exp2_fix is not None:
  1119. popt[3] = self.exp2_fix
  1120. lower_bounds_l = list(ap_bounds[0])
  1121. upper_bounds_l = list(ap_bounds[1])
  1122. lower_bounds_l[3] = self.exp2_fix - self.exp2_pr
  1123. upper_bounds_l[3] = self.exp2_fix + self.exp2_pr
  1124. ap_bounds = (tuple(lower_bounds_l), tuple(upper_bounds_l))
  1125. if self.off_fix is not None:
  1126. popt[0] = self.off_fix
  1127. lower_bounds_l = list(ap_bounds[0])
  1128. upper_bounds_l = list(ap_bounds[1])
  1129. lower_bounds_l[0] = self.off_fix - self.off_pr
  1130. upper_bounds_l[0] = self.off_fix + self.off_pr
  1131. ap_bounds = (tuple(lower_bounds_l), tuple(upper_bounds_l))
  1132. ######added optional fixation of ap parameters ##############################
  1133. elif self.aperiodic_mode == '3exp':
  1134. ap_bounds = ((-np.inf,self.freq_range[0], self.freq_range[0], 0 , 0, 0), (np.inf, self.freq_range[1], self.freq_range[1], np.inf , np.inf, np.inf))
  1135. if self.freq_range[1] < 400:
  1136. ap_bounds = ((-np.inf,self.freq_range[0], self.freq_range[0], 0 , 0, 0), (np.inf, 490, 490, np.inf , np.inf, np.inf))
  1137. ######added optional fixation of ap parameters ##############################
  1138. if self.knee_fix is not None:
  1139. popt[1] = self.knee_fix
  1140. lower_bounds_l = list(ap_bounds[0])
  1141. upper_bounds_l = list(ap_bounds[1])
  1142. lower_bounds_l[1] = self.knee_fix - self.knee_pr
  1143. if lower_bounds_l[1] < self.freq_range[0]:
  1144. lower_bounds_l[1] = self.freq_range[0]
  1145. upper_bounds_l[1] = self.knee_fix + self.knee_pr
  1146. upper_bounds_l[1] = self.knee_fix + self.knee_pr
  1147. ap_bounds = (tuple(lower_bounds_l), tuple(upper_bounds_l))
  1148. if self.exp1_fix is not None:
  1149. popt[3] = self.exp1_fix
  1150. lower_bounds_l = list(ap_bounds[0])
  1151. upper_bounds_l = list(ap_bounds[1])
  1152. lower_bounds_l[3] = self.exp1_fix - self.exp1_pr
  1153. upper_bounds_l[3] = self.exp1_fix + self.exp1_pr
  1154. ap_bounds = (tuple(lower_bounds_l), tuple(upper_bounds_l))
  1155. if self.exp2_fix is not None:
  1156. popt[4] = self.exp2_fix
  1157. lower_bounds_l = list(ap_bounds[0])
  1158. upper_bounds_l = list(ap_bounds[1])
  1159. lower_bounds_l[4] = self.exp2_fix - self.exp2_pr
  1160. upper_bounds_l[4] = self.exp2_fix + self.exp2_pr
  1161. ap_bounds = (tuple(lower_bounds_l), tuple(upper_bounds_l))
  1162. if self.off_fix is not None:
  1163. popt[0] = self.off_fix
  1164. lower_bounds_l = list(ap_bounds[0])
  1165. upper_bounds_l = list(ap_bounds[1])
  1166. lower_bounds_l[0] = self.off_fix - self.off_pr
  1167. upper_bounds_l[0] = self.off_fix + self.off_pr
  1168. ap_bounds = (tuple(lower_bounds_l), tuple(upper_bounds_l))
  1169. ######added optional fixation of ap parameters ##############################
  1170. elif self.aperiodic_mode == '2exp_flat':
  1171. ap_bounds = ((-np.inf, self.freq_range[0], self.freq_range[0], 0, 0), (np.inf, self.freq_range[1], self.freq_range[1], np.inf, np.inf))
  1172. if self.freq_range[1] < 400:
  1173. ap_bounds = ((-np.inf, self.freq_range[0], self.freq_range[0], 0, 0), (np.inf, 490, 490, np.inf, np.inf))
  1174. ######added optional fixation of ap parameters ##############################
  1175. if self.knee_fix is not None:
  1176. popt[1] = self.knee_fix
  1177. lower_bounds_l = list(ap_bounds[0])
  1178. upper_bounds_l = list(ap_bounds[1])
  1179. lower_bounds_l[1] = self.knee_fix - self.knee_pr
  1180. if lower_bounds_l[1] < self.freq_range[0]:
  1181. lower_bounds_l[1] = self.freq_range[0]
  1182. upper_bounds_l[1] = self.knee_fix + self.knee_pr
  1183. upper_bounds_l[1] = self.knee_fix + self.knee_pr
  1184. ap_bounds = (tuple(lower_bounds_l), tuple(upper_bounds_l))
  1185. if self.exp1_fix is not None:
  1186. popt[3] = self.exp1_fix
  1187. lower_bounds_l = list(ap_bounds[0])
  1188. upper_bounds_l = list(ap_bounds[1])
  1189. lower_bounds_l[3] = self.exp1_fix - self.exp1_pr
  1190. upper_bounds_l[3] = self.exp1_fix + self.exp1_pr
  1191. ap_bounds = (tuple(lower_bounds_l), tuple(upper_bounds_l))
  1192. if self.exp2_fix is not None:
  1193. popt[4] = self.exp2_fix
  1194. lower_bounds_l = list(ap_bounds[0])
  1195. upper_bounds_l = list(ap_bounds[1])
  1196. lower_bounds_l[4] = self.exp2_fix - self.exp2_pr
  1197. upper_bounds_l[4] = self.exp2_fix + self.exp2_pr
  1198. ap_bounds = (tuple(lower_bounds_l), tuple(upper_bounds_l))
  1199. if self.off_fix is not None:
  1200. popt[0] = self.off_fix
  1201. lower_bounds_l = list(ap_bounds[0])
  1202. upper_bounds_l = list(ap_bounds[1])
  1203. lower_bounds_l[0] = self.off_fix - self.off_pr
  1204. upper_bounds_l[0] = self.off_fix + self.off_pr
  1205. ap_bounds = (tuple(lower_bounds_l), tuple(upper_bounds_l))
  1206. ######added optional fixation of ap parameters ##############################
  1207. elif self.aperiodic_mode == 'flat_1exp':
  1208. #ap_bounds = tuple(bound[0:3] for bound in self._ap_bounds) # Assuming _ap_bounds already correctly structured for 'knee_1exp'
  1209. ap_bounds = ((-np.inf, (self.freq_range[0]), 0), (np.inf, (self.freq_range[1]), np.inf))
  1210. elif self.aperiodic_mode == 'fixed':
  1211. ap_bounds = tuple(bound[0:2] for bound in self._ap_bounds)
  1212. else:
  1213. raise ValueError(f"Unknown aperiodic mode: {self.aperiodic_mode}")
  1214. # Second aperiodic fit - using results of first fit as guess parameters
  1215. # See note in _simple_ap_fit about warnings
  1216. for g in range(0, len(popt)):
  1217. if popt[g] < ap_bounds[0][g]:
  1218. popt[g] = ap_bounds[0][g]
  1219. elif popt[g] > ap_bounds[1][g]:
  1220. popt[g] = ap_bounds[1][g]
  1221. aperiodic_params, _ = curve_fit(get_ap_func(self.aperiodic_mode),
  1222. freqs_ignore, spectrum_ignore, p0=popt,
  1223. maxfev=self._maxfev, bounds=ap_bounds,
  1224. ftol=self._tol, xtol=self._tol, gtol=self._tol,
  1225. check_finite=False)
  1226. if self.aperiodic_mode == '2exp_flat' or self.aperiodic_mode == '3exp':
  1227. if aperiodic_params[1] > aperiodic_params[2]:
  1228. knee_copy = aperiodic_params[1].copy()
  1229. flat_freq_copy = aperiodic_params[2].copy()
  1230. aperiodic_params[1] = flat_freq_copy
  1231. aperiodic_params[2] = knee_copy
  1232. elif self.aperiodic_mode == '2exp':
  1233. if aperiodic_params[2] > aperiodic_params[3]:
  1234. exp1_copy = aperiodic_params[2].copy()
  1235. exp2_copy = aperiodic_params[3].copy()
  1236. aperiodic_params[2] = exp2_copy
  1237. aperiodic_params[3] = exp1_copy
  1238. self.current_ap_fit_params = aperiodic_params #added
  1239. return aperiodic_params
  1240. ##################################################inserted#########################################################################################
  1241. def rem_electric_noise(self, power_spectrum, freqs):
  1242. """
  1243. Remove harmonics and electric noise from the power spectrum.
  1244. Parameters
  1245. ----------
  1246. freqs : 1d array
  1247. Frequency values for the power_spectrum, in linear scale.
  1248. power_spectrum : 1d array
  1249. Power values, in log10 scale.
  1250. Returns
  1251. -------
  1252. power_spectrum : 1d array
  1253. Power spectrum filtered from noise.
  1254. """
  1255. maxfevs = 5000 # The maximum number of calls to the curve fitting function
  1256. _tol = 0.00001 # Here reduce tolerance to speed fitting. Set value to 1e-8 to match curve_fit default
  1257. power_spectrum = np.log10(power_spectrum)
  1258. # first of all: remove electric noise and its harmonics
  1259. noise_bands = [[43, 57], [130, 170], [12.5, 22]]
  1260. correnction_bands = [[48.5, 51.5], [148, 152], [13.5, 21.5]]
  1261. noise_std_limits = [0, 4]
  1262. #second: fit gaussian peaks at bands having harmonics or electric noise
  1263. ap_mode = 'fixed'
  1264. ap_bounds_n = ((-np.inf, -np.inf), (np.inf, np.inf))
  1265. for noise_band, correction_band in zip(noise_bands, correnction_bands):
  1266. # calculate guess parameters
  1267. freq_res = freqs[1] - freqs[0]
  1268. start_index1 = round(noise_band[0]/freq_res)
  1269. end_index1 = round(noise_band[1]/freq_res)
  1270. f1 = freqs[start_index1:end_index1]
  1271. u1 = np.log10(f1)
  1272. p1 = power_spectrum[start_index1:end_index1]
  1273. guess_exp1 = [(p1[-1] - p1[0])/(u1[0]-u1[-1])]
  1274. off1 = (guess_exp1[0]*u1[0]) + p1[0]
  1275. f_mask_n = np.logical_and(freqs >= noise_band[0], freqs <= noise_band[1])
  1276. freqs_n = freqs[f_mask_n]
  1277. f_mask_correction = np.logical_and(freqs >= correction_band[0], freqs <= correction_band[1])
  1278. freqs_correction = freqs[f_mask_correction]
  1279. psd_n = power_spectrum[f_mask_n]
  1280. n_off_guess = [off1]
  1281. n_exp_guess = guess_exp1
  1282. noise_guess = np.array(n_off_guess + n_exp_guess)
  1283. noise_ap_params0, _ = curve_fit(get_ap_func(ap_mode),
  1284. freqs_n, psd_n, p0=noise_guess,
  1285. maxfev=maxfevs, bounds=ap_bounds_n,
  1286. ftol=_tol, xtol=_tol, gtol=_tol,
  1287. check_finite=False)
  1288. initial_noise_fit = noise_ap_params0[0] - np.log10(freqs_n**noise_ap_params0[1])
  1289. # flatten and remove outliers
  1290. flatspec = psd_n - initial_noise_fit
  1291. flatspec[flatspec < 0] = 0 # Flatten outliers, defined as any points that drop below 0
  1292. perc_thresh = np.percentile(flatspec, 0.025) # -> remove top 2.5% of power values, don't be confused that it rounds to 0
  1293. perc_mask = flatspec <= perc_thresh
  1294. freqs_n_ignore = freqs_n[perc_mask]
  1295. psd_n_ignore = psd_n[perc_mask]
  1296. noise_ap_params1, _ = curve_fit(get_ap_func(ap_mode),
  1297. freqs_n_ignore, psd_n_ignore, p0=noise_ap_params0,
  1298. maxfev=maxfevs, bounds=ap_bounds_n,
  1299. ftol=_tol, xtol=_tol, gtol=_tol,
  1300. check_finite=False)
  1301. out_rem_noise_fit = noise_ap_params1[0] - np.log10(freqs_n**noise_ap_params1[1])
  1302. flat_noise_spec = psd_n - out_rem_noise_fit
  1303. flat_noise_with_peak = psd_n - out_rem_noise_fit # copy for later
  1304. # calculate noise gaussian peak
  1305. guess_n_peak = np.empty([0, 3])
  1306. max_ind = np.argmax(flat_noise_spec)
  1307. max_height = flat_noise_spec[max_ind]
  1308. guess_freq = freqs_n[max_ind]
  1309. guess_height = max_height
  1310. if guess_height > 0.01: # min_peak_height arbitrarily chosen to be 0.2
  1311. if not max_height <= 0.1 * np.std(flat_noise_spec): # relitive threshold arbitrarily chosen to be 1
  1312. half_height = 0.5 * max_height
  1313. le_ind = next((val for val in range(max_ind - 1, 0, -1)
  1314. if flat_noise_spec[val] <= half_height), None)
  1315. ri_ind = next((val for val in range(max_ind + 1, len(flat_noise_spec), 1)
  1316. if flat_noise_spec[val] <= half_height), None)
  1317. try:
  1318. short_side = min([abs(ind - max_ind) \
  1319. for ind in [le_ind, ri_ind] if ind is not None])
  1320. fwhm = short_side * 2 * freq_res
  1321. guess_std = fwhm / (2 * np.sqrt(2 * np.log(2)))
  1322. except ValueError:
  1323. guess_std = np.mean(noise_std_limits)*2
  1324. if guess_std < noise_std_limits[0]:
  1325. guess_std = noise_std_limits[0]
  1326. if guess_std > noise_std_limits[1]:
  1327. guess_std = noise_std_limits[1]
  1328. guess_n_peak = np.vstack((guess_n_peak, (guess_freq, guess_height, guess_std)))
  1329. peak_gauss = gaussian_function(freqs_n, guess_freq, guess_height, guess_std)
  1330. flat_noise_spec = flat_noise_spec - peak_gauss
  1331. if len(guess_n_peak) > 0:
  1332. lo_bound = [[peak[0] - 2 * 0.5 * peak[2], 0, noise_std_limits[0]] # 1.5 = self._cf_bound
  1333. for peak in guess_n_peak]
  1334. hi_bound = [[peak[0] + 2 * 0.5 * peak[2], np.inf, noise_std_limits[1]]
  1335. for peak in guess_n_peak]
  1336. lo_bound = [bound if bound[0] > noise_band[0] else \
  1337. [noise_band[0], *bound[1:]] for bound in lo_bound]
  1338. hi_bound = [bound if bound[0] < noise_band[1] else \
  1339. [noise_band[1], *bound[1:]] for bound in hi_bound]
  1340. gaus_param_bounds = (tuple(item for sublist in lo_bound for item in sublist),
  1341. tuple(item for sublist in hi_bound for item in sublist))
  1342. # Flatten guess, for use with curve fit
  1343. guess_n_peak = np.ndarray.flatten(guess_n_peak)
  1344. # fit_noise_peak
  1345. noise_params, _ = curve_fit(gaussian_function, freqs_n, flat_noise_with_peak,
  1346. p0=guess_n_peak, maxfev=maxfevs, bounds=gaus_param_bounds,
  1347. ftol=_tol, xtol=_tol, gtol=_tol,
  1348. check_finite=False, jac=jacobian_gauss)
  1349. noise_peak = gaussian_function(freqs_n, noise_params[0], noise_params[1], noise_params[2])
  1350. psd_n_peak_rem = psd_n - noise_peak
  1351. noise_ap_params2, _ = curve_fit(get_ap_func(ap_mode),
  1352. freqs_n, psd_n_peak_rem, p0=noise_ap_params1,
  1353. maxfev=maxfevs, bounds=ap_bounds_n,
  1354. ftol=_tol, xtol=_tol, gtol=_tol,
  1355. check_finite=False)
  1356. iters_noise_fit = noise_ap_params2[0] - np.log10(freqs_n**noise_ap_params2[1])
  1357. n_iterations = 12
  1358. for it in range(1, n_iterations +1):
  1359. if np.all(noise_params != [0, 0, 0]):
  1360. flat_noise_with_peak = psd_n - iters_noise_fit
  1361. noise_params, _ = curve_fit(gaussian_function, freqs_n, flat_noise_with_peak,
  1362. p0=noise_params, maxfev=maxfevs, bounds=gaus_param_bounds,
  1363. ftol=_tol, xtol=_tol, gtol=_tol,
  1364. check_finite=False, jac=jacobian_gauss)
  1365. noise_peak = gaussian_function(freqs_n, noise_params[0], noise_params[1], noise_params[2])
  1366. psd_n_peak_rem = psd_n - noise_peak
  1367. noise_ap_params2, _ = curve_fit(get_ap_func(ap_mode),
  1368. freqs_n, psd_n_peak_rem, p0=noise_ap_params2,
  1369. maxfev=maxfevs, bounds=ap_bounds_n,
  1370. ftol=_tol, xtol=_tol, gtol=_tol,
  1371. check_finite=False)
  1372. iters_noise_fit = noise_ap_params2[0] - np.log10(freqs_n**noise_ap_params2[1])
  1373. power_spectrum[f_mask_n] = power_spectrum[f_mask_n] - noise_peak #remove the noise peak from the original power spectrum
  1374. power_spectrum = 10**power_spectrum
  1375. return power_spectrum
  1376. def _fit_peaks(self, flat_iter):
  1377. """
  1378. Iteratively fit peaks to flattened spectrum.
  1379. Parameters
  1380. ----------
  1381. flat_iter : 1d array
  1382. Flattened power spectrum values.
  1383. Returns
  1384. -------
  1385. gaussian_params : 2d array
  1386. Parameters that define the gaussian fit(s).
  1387. Each row is a gaussian, as [mean, height, standard deviation].
  1388. """
  1389. # Define frequency bands ranges and corresponding parameters
  1390. self.std_limits = np.empty([0 , 2])
  1391. self.cf_bounds = np.empty([0 , 2])
  1392. theta_range = [self.freq_range[0], 19]
  1393. sgamma_range = [19, 48]
  1394. fgamma_range = [51, 90]
  1395. ripple_range = [100, 200]
  1396. list_of_ranges = [ripple_range, fgamma_range, theta_range, sgamma_range]
  1397. list_of_thresholds = [1, 1, 1, 1] #equivalent to peak_threshold in the original FOOOF library
  1398. abs_thresholds = [ 0.05, 0.08, 0.1, 0.05] #equivalent to min_peak_height in the original FOOOF library
  1399. list_of_peak_widths = [ [10, 30], [3, 35], [2, 5], [5, 20]] #equivalent to peak_width_limits in the original FOOOF library
  1400. allowed_ranges = [[125, 160], [52, 80], [5, 9.5], [25, 40]] #fitting ranges for different frequency bands
  1401. # Initialize matrix of guess parameters for gaussian fitting
  1402. guess = np.empty([0, 3])
  1403. #Parameters for frequency range higher than 400 Hz
  1404. if self.freq_range[1] >= 400:
  1405. additional_peak_range = [205, 495]
  1406. list_of_ranges = [ripple_range, fgamma_range, theta_range, sgamma_range, additional_peak_range]
  1407. list_of_thresholds = [1, 1, 1, 1, 1]
  1408. abs_thresholds = [0.05, 0.08, 0.1, 0.05, 0.05]
  1409. list_of_peak_widths = [ [10, 30], [3, 35], [2, 5], [5, 20], [50, 150]]
  1410. allowed_ranges = [[125, 160], [52, 80], [5, 9.5], [25, 47], [220, 380]]
  1411. elif self.freq_range[1] <= 120:
  1412. # for 4-100Hz:
  1413. list_of_ranges = [fgamma_range, theta_range, sgamma_range]
  1414. list_of_thresholds = [1, 1, 1]
  1415. abs_thresholds = [0.08, 0.1, 0.05]
  1416. list_of_peak_widths = [[3, 35], [2, 5], [5, 20]]
  1417. allowed_ranges = [[52, 80], [5, 9.5], [25, 47]]
  1418. # Find peak: Loop through, finding a candidate peak, and fitting with a guess gaussian
  1419. # Stopping procedures: limit on # of peaks, or relative or absolute height thresholds
  1420. for allowed_range, current_range, current_threshold, current_width, abs_threshold in zip(allowed_ranges, list_of_ranges, list_of_thresholds, list_of_peak_widths, abs_thresholds):
  1421. current_range_indeces = (self.freqs >= current_range[0]) & (self.freqs <= current_range[1])
  1422. current_band = flat_iter[current_range_indeces]
  1423. current_freqs = self.freqs[current_range_indeces]
  1424. #new change for better peak estimation
  1425. allowed_range_indeces = (self.freqs >= allowed_range[0]) & (self.freqs <= allowed_range[1])
  1426. allowed_band = flat_iter[allowed_range_indeces]
  1427. allowed_freqs = self.freqs[allowed_range_indeces]
  1428. max_ind = np.argmax(allowed_band)
  1429. guess_freq = allowed_freqs[max_ind]
  1430. max_ind = int(np.round((guess_freq - current_range[0])/self.freq_res)-1)
  1431. max_height = current_band[max_ind]
  1432. guess_height = max_height
  1433. guess_freq = current_freqs[max_ind]
  1434. # Stop searching for peaks once height drops below height threshold
  1435. if max_height <= current_threshold * np.std(current_band):
  1436. continue
  1437. if not guess_height > abs_threshold: # if 0 -> not absolute threshold
  1438. continue
  1439. half_height = 0.5 * max_height
  1440. le_ind = next((val for val in range(max_ind - 1, 0, -1) if current_band[val] <= half_height), None)
  1441. ri_ind = next((val for val in range(max_ind + 1, len(current_band), 1) if current_band[val] <= half_height), None)
  1442. # Guess bandwidth procedure: estimate the width of the peak
  1443. try:
  1444. # Get an estimated width from the shortest side of the peak
  1445. # We grab shortest to avoid estimating very large values from overlapping peaks
  1446. # Grab the shortest side, ignoring a side if the half max was not found
  1447. short_side = min([abs(ind - max_ind) \
  1448. for ind in [le_ind, ri_ind] if ind is not None])
  1449. # Use the shortest side to estimate full-width, half max (converted to Hz)
  1450. # and use this to estimate that guess for gaussian standard deviation
  1451. fwhm = short_side * 2 * self.freq_res
  1452. guess_std = compute_gauss_std(fwhm)
  1453. except ValueError:
  1454. # This procedure can fail (very rarely), if both left & right inds end up as None
  1455. # In this case, default the guess to the average of the peak width limits
  1456. guess_std = np.mean(current_width)
  1457. # Check that guess value isn't outside preset limits - restrict if so
  1458. # Note: without this, curve_fitting fails if given guess > or < bounds
  1459. if guess_std < current_width[0]/2:
  1460. guess_std = current_width[0]/2
  1461. if guess_std > current_width[1]/2:
  1462. guess_std = current_width[1]/2
  1463. gauss_limit = [0,0]
  1464. gauss_limit[0] = current_width[0]/2
  1465. gauss_limit[1] = current_width[1]/2
  1466. self.std_limits = np.vstack((self.std_limits, gauss_limit))
  1467. self.cf_bounds = np.vstack((self.cf_bounds, allowed_range))
  1468. # Collect guess parameters and subtract this guess gaussian from the data
  1469. guess = np.vstack((guess, (guess_freq, guess_height, guess_std)))
  1470. peak_gauss = gaussian_function(current_freqs, guess_freq, guess_height, guess_std)
  1471. current_band = current_band - peak_gauss
  1472. flat_iter[current_range_indeces] = current_band
  1473. # Check peaks based on edges, and on overlap, dropping any that violate requirements
  1474. # If there are peak guesses, fit the peaks, and sort results
  1475. if len(guess) > 0:
  1476. gaussian_params = self._fit_peak_guess(guess)
  1477. gaussian_params = gaussian_params[gaussian_params[:, 0].argsort()]
  1478. else:
  1479. gaussian_params = np.empty([0, 3])
  1480. return gaussian_params
  1481. def _fit_peak_guess(self, guess):
  1482. """
  1483. Fits a group of peak guesses with a fit function.
  1484. Parameters
  1485. ----------
  1486. guess : 2d array, shape=[n_peaks, 3]
  1487. Guess parameters for gaussian fits to peaks, as gaussian parameters.
  1488. Returns
  1489. -------
  1490. gaussian_params : 2d array, shape=[n_peaks, 3]
  1491. Parameters for gaussian fits to peaks, as gaussian parameters.
  1492. """
  1493. # Set the bounds for CF, enforce positive height value, and set bandwidth limits
  1494. # Note that 'guess' is in terms of gaussian std, so +/- BW is 2 * the guess_gauss_std
  1495. # This set of list comprehensions is a way to end up with bounds in the form:
  1496. # ((cf_low_peak1, height_low_peak1, bw_low_peak1, *repeated for n_peaks*),
  1497. # (cf_high_peak1, height_high_peak1, bw_high_peak, *repeated for n_peaks*))
  1498. # ^where each value sets the bound on the specified parameter
  1499. num_peaks = guess.shape[0]
  1500. # Initialize self.std_limits if it is empty
  1501. # Debugging output to check the input sizes
  1502. if self.std_limits.shape[0] != num_peaks:
  1503. raise ValueError("The number of peaks in 'guess' does not match the number of std limits provided.")
  1504. # Set the bounds for CF, enforce positive height value, and set bandwidth limits
  1505. lo_bound = [[cf[0], 0, std_lim[0]] ## Default was 2* self.cf_bound
  1506. for cf, peak, std_lim in zip(self.cf_bounds , guess, self.std_limits)]
  1507. hi_bound = [[cf[1], np.inf, std_lim[1]]
  1508. for cf, peak, std_lim in zip(self.cf_bounds , guess, self.std_limits)]
  1509. # Check that CF bounds are within frequency range
  1510. lo_bound = [bound if bound[0] > self.freq_range[0] else \
  1511. [self.freq_range[0], *bound[1:]] for bound in lo_bound]
  1512. hi_bound = [bound if bound[0] < self.freq_range[1] else \
  1513. [self.freq_range[1], *bound[1:]] for bound in hi_bound]
  1514. # Unpacks the embedded lists into flat tuples
  1515. gaus_param_bounds = (tuple(item for sublist in lo_bound for item in sublist),
  1516. tuple(item for sublist in hi_bound for item in sublist))
  1517. # Flatten guess, for use with curve fit
  1518. for a in range(0, len(guess)):
  1519. peak_a = guess[a]
  1520. hi_bound_a = hi_bound[a]
  1521. lo_bound_a = lo_bound[a]
  1522. for b in range(0, 3):
  1523. p_b = peak_a[b]
  1524. hi_b = hi_bound_a[b]
  1525. lo_b = lo_bound_a[b]
  1526. if p_b < lo_b:
  1527. guess[a][b] = lo_b
  1528. if p_b > hi_b:
  1529. guess[a][b] = hi_b
  1530. guess_flat = np.ndarray.flatten(guess)
  1531. # Check if the initial guess is within the bounds
  1532. for i, g in enumerate(guess_flat):
  1533. if not (gaus_param_bounds[0][i] <= g <= gaus_param_bounds[1][i]):
  1534. raise ValueError(f"Initial guess {g} at index {i} is out of bounds: "
  1535. f"{gaus_param_bounds[0][i]}, {gaus_param_bounds[1][i]}")
  1536. # Fit the peaks
  1537. try:
  1538. gaussian_params, _ = curve_fit(gaussian_function, self.freqs, self._spectrum_flat,
  1539. p0=guess_flat, maxfev=self._maxfev, bounds=gaus_param_bounds,
  1540. ftol=self._tol, xtol=self._tol, gtol=self._tol,
  1541. check_finite=False, jac=jacobian_gauss)
  1542. except RuntimeError as excp:
  1543. error_msg = ("Model fitting failed due to not finding "
  1544. "parameters in the peak component fit.")
  1545. raise FitError(error_msg) from excp
  1546. except LinAlgError as excp:
  1547. error_msg = ("Model fitting failed due to a LinAlgError during peak fitting. "
  1548. "This can happen with settings that are too liberal, leading, "
  1549. "to a large number of guess peaks that cannot be fit together.")
  1550. raise FitError(error_msg) from excp
  1551. # Re-organize params into 2d matrix
  1552. gaussian_params = np.array(group_three(gaussian_params))
  1553. return gaussian_params
  1554. def _create_peak_params(self, gaus_params):
  1555. """Copies over the gaussian params to peak outputs, updating as appropriate.
  1556. Parameters
  1557. ----------
  1558. gaus_params : 2d array
  1559. Parameters that define the gaussian fit(s), as gaussian parameters.
  1560. Returns
  1561. -------
  1562. peak_params : 2d array
  1563. Fitted parameter values for the peaks, with each row as [CF, PW, BW].
  1564. Notes
  1565. -----
  1566. The gaussian center is unchanged as the peak center frequency.
  1567. The gaussian height is updated to reflect the height of the peak above
  1568. the aperiodic fit. This is returned instead of the gaussian height, as
  1569. the gaussian height is harder to interpret, due to peak overlaps.
  1570. The gaussian standard deviation is updated to be 'both-sided', to reflect the
  1571. 'bandwidth' of the peak, as opposed to the gaussian parameter, which is 1-sided.
  1572. Performing this conversion requires that the model has been run,
  1573. with `freqs`, `fooofed_spectrum_` and `_ap_fit` all required to be available.
  1574. """
  1575. peak_params = np.empty((len(gaus_params), 3))
  1576. for ii, peak in enumerate(gaus_params):
  1577. # Gets the index of the power_spectrum at the frequency closest to the CF of the peak
  1578. ind = np.argmin(np.abs(self.freqs - peak[0]))
  1579. # Collect peak parameter data
  1580. peak_params[ii] = [peak[0], self.fooofed_spectrum_[ind] - self._ap_fit[ind],
  1581. peak[2] * 2]
  1582. return peak_params
  1583. def _drop_peak_cf(self, guess):
  1584. """Check whether to drop peaks based on center's proximity to the edge of the spectrum.
  1585. Parameters
  1586. ----------
  1587. guess : 2d array
  1588. Guess parameters for gaussian peak fits. Shape: [n_peaks, 3].
  1589. Returns
  1590. -------
  1591. guess : 2d array
  1592. Guess parameters for gaussian peak fits. Shape: [n_peaks, 3].
  1593. """
  1594. cf_params = guess[:, 0]
  1595. bw_params = guess[:, 2] * self._bw_std_edge
  1596. # Check if peaks within drop threshold from the edge of the frequency range
  1597. keep_peak = \
  1598. (np.abs(np.subtract(cf_params, self.freq_range[0])) > bw_params) & \
  1599. (np.abs(np.subtract(cf_params, self.freq_range[1])) > bw_params)
  1600. # Ensure self.std_limits is a numpy array
  1601. self.std_limits = np.array(self.std_limits)
  1602. self.cf_bounds = np.array(self.cf_bounds)
  1603. # Drop peaks that fail the center frequency edge criterion
  1604. guess = guess[keep_peak]
  1605. self.std_limits = self.std_limits[keep_peak]
  1606. self.cf_bounds = self.cf_bounds[keep_peak]
  1607. return guess, self.std_limits , self.cf_bounds
  1608. def _drop_peak_overlap(self, guess):
  1609. """Checks whether to drop gaussians based on amount of overlap.
  1610. Parameters
  1611. ----------
  1612. guess : 2d array
  1613. Guess parameters for gaussian peak fits. Shape: [n_peaks, 3].
  1614. Returns
  1615. -------
  1616. guess : 2d array
  1617. Guess parameters for gaussian peak fits. Shape: [n_peaks, 3].
  1618. std_limits : list of lists
  1619. Filtered standard deviation limits corresponding to the kept peaks.
  1620. Notes
  1621. -----
  1622. For any gaussians with an overlap that crosses the threshold,
  1623. the lowest height guess Gaussian is dropped.
  1624. """
  1625. # Ensure guess is a numpy array
  1626. # Ensure guess is a numpy array
  1627. guess = np.array(guess)
  1628. # Ensure std_limits is a numpy array
  1629. self.std_limits = np.array(self.std_limits)
  1630. # Debugging output to check the structure of guess and std_limits
  1631. # Check if the dimensions are correct
  1632. if guess.ndim != 2 or guess.shape[1] != 3:
  1633. raise ValueError("Expected 'guess' to be a 2D array with shape [n_peaks, 3].")
  1634. if self.std_limits.ndim != 2 or self.std_limits.shape[1] != 2:
  1635. raise ValueError("Expected 'std_limits' to be a 2D array with shape [n_peaks, 2].")
  1636. if len(guess) != len(self.std_limits):
  1637. raise ValueError("The number of peaks in 'guess' does not match the number of std limits provided.")
  1638. # Sort the peak guesses by increasing frequency and sort std_limits in the same order
  1639. sorted_indices = np.argsort(guess[:, 0])
  1640. guess = guess[sorted_indices]
  1641. self.std_limits = self.std_limits[sorted_indices]
  1642. self.cf_bounds = self.cf_bounds[sorted_indices]
  1643. # Debugging output to check the sorted guess and std_limits
  1644. # Calculate standard deviation bounds for checking amount of overlap
  1645. # The bounds are the gaussian frequency +/- gaussian standard deviation
  1646. bounds = [[peak[0] - peak[2] * self._gauss_overlap_thresh,
  1647. peak[0] + peak[2] * self._gauss_overlap_thresh] for peak in guess]
  1648. # Debugging output to check the bounds
  1649. # Loop through peak bounds, comparing current bound to that of next peak
  1650. # If the left peak's upper bound extends pass the right peaks lower bound,
  1651. # then drop the Gaussian with the lower height
  1652. drop_inds = []
  1653. for ind, b_0 in enumerate(bounds[:-1]):
  1654. b_1 = bounds[ind + 1]
  1655. # Check if bound of current peak extends into next peak
  1656. if b_0[1] > b_1[0]:
  1657. # If so, get the index of the gaussian with the lowest height (to drop)
  1658. drop_inds.append([ind, ind + 1][np.argmin([guess[ind][1], guess[ind + 1][1]])])
  1659. # Drop any peaks guesses that overlap too much, based on threshold
  1660. keep_peak = np.array([ind not in drop_inds for ind in range(len(guess))], dtype=bool)
  1661. guess = guess[keep_peak]
  1662. self.std_limits = self.std_limits[keep_peak]
  1663. self.cf_bounds = self.cf_bounds[keep_peak]
  1664. # Check if the dimensions are correct
  1665. if guess.ndim != 2 or guess.shape[1] != 3:
  1666. raise ValueError("Expected 'guess' to be a 2D array with shape [n_peaks, 3].")
  1667. if self.std_limits.ndim != 2 or self.std_limits.shape[1] != 2:
  1668. raise ValueError("Expected 'std_limits' to be a 2D array with shape [n_peaks, 2].")
  1669. if len(guess) != len(self.std_limits):
  1670. raise ValueError("The number of peaks in 'guess' does not match the number of std limits provided.")
  1671. ##############edit this return later you don't need to do this #################
  1672. return guess, self.std_limits , self.cf_bounds
  1673. def _calc_r_squared(self):
  1674. """Calculate the r-squared goodness of fit of the model, compared to the original data."""
  1675. r_val = np.corrcoef(self.power_spectrum, self.fooofed_spectrum_)
  1676. self.r_squared_ = r_val[0][1] ** 2
  1677. def _calc_error(self, metric=None):
  1678. """Calculate the overall error of the model fit, compared to the original data.
  1679. Parameters
  1680. ----------
  1681. metric : {'MAE', 'MSE', 'RMSE'}, optional
  1682. Which error measure to calculate:
  1683. * 'MAE' : mean absolute error
  1684. * 'MSE' : mean squared error
  1685. * 'RMSE' : root mean squared error
  1686. Raises
  1687. ------
  1688. ValueError
  1689. If the requested error metric is not understood.
  1690. Notes
  1691. -----
  1692. Which measure is applied is by default controlled by the `_error_metric` attribute.
  1693. """
  1694. # If metric is not specified, use the default approach
  1695. metric = self._error_metric if not metric else metric
  1696. if metric == 'AP':
  1697. self.error_ = np.abs(self.power_spectrum - self._ap_fit).mean()
  1698. if metric == 'AP_squared':
  1699. self.error_ = np.mean((self.power_spectrum - self._ap_fit)**2)
  1700. if metric == 'MAE':
  1701. self.error_ = np.abs(self.power_spectrum - self.fooofed_spectrum_).mean()
  1702. elif metric == 'MSE':
  1703. self.error_ = ((self.power_spectrum - self.fooofed_spectrum_) ** 2).mean()
  1704. elif metric == 'RMSE':
  1705. self.error_ = np.sqrt(((self.power_spectrum - self.fooofed_spectrum_) ** 2).mean())
  1706. def _prepare_data(self, freqs, power_spectrum, freq_range, spectra_dim=1):
  1707. """Prepare input data for adding to current object.
  1708. Parameters
  1709. ----------
  1710. freqs : 1d array
  1711. Frequency values for the power_spectrum, in linear space.
  1712. power_spectrum : 1d or 2d array
  1713. Power values, which must be input in linear space.
  1714. 1d vector, or 2d as [n_power_spectra, n_freqs].
  1715. freq_range : list of [float, float]
  1716. Frequency range to restrict power spectrum to. If None, keeps the entire range.
  1717. spectra_dim : int, optional, default: 1
  1718. Dimensionality that the power spectra should have.
  1719. Returns
  1720. -------
  1721. freqs : 1d array
  1722. Frequency values for the power_spectrum, in linear space.
  1723. power_spectrum : 1d or 2d array
  1724. Power spectrum values, in log10 scale.
  1725. 1d vector, or 2d as [n_power_specta, n_freqs].
  1726. freq_range : list of [float, float]
  1727. Minimum and maximum values of the frequency vector.
  1728. freq_res : float
  1729. Frequency resolution of the power spectrum.
  1730. Raises
  1731. ------
  1732. DataError
  1733. If there is an issue with the data.
  1734. InconsistentDataError
  1735. If the input data are inconsistent size.
  1736. """
  1737. # Check that data are the right types
  1738. if not isinstance(freqs, np.ndarray) or not isinstance(power_spectrum, np.ndarray):
  1739. raise DataError("Input data must be numpy arrays.")
  1740. # Check that data have the right dimensionality
  1741. if freqs.ndim != 1 or (power_spectrum.ndim != spectra_dim):
  1742. raise DataError("Inputs are not the right dimensions.")
  1743. # Check that data sizes are compatible
  1744. if freqs.shape[-1] != power_spectrum.shape[-1]:
  1745. raise InconsistentDataError("The input frequencies and power spectra "
  1746. "are not consistent size.")
  1747. # Check if power values are complex
  1748. if np.iscomplexobj(power_spectrum):
  1749. raise DataError("Input power spectra are complex values. "
  1750. "FOOOF does not currently support complex inputs.")
  1751. # Force data to be dtype of float64
  1752. # If they end up as float32, or less, scipy curve_fit fails (sometimes implicitly)
  1753. if freqs.dtype != 'float64':
  1754. freqs = freqs.astype('float64')
  1755. if power_spectrum.dtype != 'float64':
  1756. power_spectrum = power_spectrum.astype('float64')
  1757. # Check frequency range, trim the power_spectrum range if requested
  1758. power_spectrum = self.rem_electric_noise(power_spectrum , freqs)
  1759. if freq_range:
  1760. freqs, power_spectrum = trim_spectrum(freqs, power_spectrum, freq_range)
  1761. # Check if freqs start at 0 and move up one value if so
  1762. # Aperiodic fit gets an inf if freq of 0 is included, which leads to an error
  1763. if freqs[0] == 0.0:
  1764. freqs, power_spectrum = trim_spectrum(freqs, power_spectrum, [freqs[1], freqs.max()])
  1765. if self.verbose:
  1766. print("\nFOOOF WARNING: Skipping frequency == 0, "
  1767. "as this causes a problem with fitting.")
  1768. # Calculate frequency resolution, and actual frequency range of the data
  1769. freq_range = [freqs.min(), freqs.max()]
  1770. freq_res = freqs[1] - freqs[0]
  1771. # Log power values
  1772. power_spectrum = np.log10(power_spectrum)
  1773. ## Data checks - run checks on inputs based on check modes
  1774. if self._check_freqs:
  1775. # Check if the frequency data is unevenly spaced, and raise an error if so
  1776. freq_diffs = np.diff(freqs)
  1777. if not np.all(np.isclose(freq_diffs, freq_res)):
  1778. raise DataError("The input frequency values are not evenly spaced. "
  1779. "The model expects equidistant frequency values in linear space.")
  1780. if self._check_data:
  1781. # Check if there are any infs / nans, and raise an error if so
  1782. if np.any(np.isinf(power_spectrum)) or np.any(np.isnan(power_spectrum)):
  1783. error_msg = ("The input power spectra data, after logging, contains NaNs or Infs. "
  1784. "This will cause the fitting to fail. "
  1785. "One reason this can happen is if inputs are already logged. "
  1786. "Inputs data should be in linear spacing, not log.")
  1787. raise DataError(error_msg)
  1788. return freqs, power_spectrum, freq_range, freq_res
  1789. def _add_from_dict(self, data):
  1790. """Add data to object from a dictionary.
  1791. Parameters
  1792. ----------
  1793. data : dict
  1794. Dictionary of data to add to self.
  1795. """
  1796. # Reconstruct object from loaded data
  1797. for key in data.keys():
  1798. setattr(self, key, data[key])
  1799. def _check_loaded_results(self, data):
  1800. """Check if results have been added and check data.
  1801. Parameters
  1802. ----------
  1803. data : dict
  1804. A dictionary of data that has been added to the object.
  1805. """
  1806. # If results loaded, check dimensions of peak parameters
  1807. # This fixes an issue where they end up the wrong shape if they are empty (no peaks)
  1808. if set(OBJ_DESC['results']).issubset(set(data.keys())):
  1809. self.peak_params_ = check_array_dim(self.peak_params_)
  1810. self.gaussian_params_ = check_array_dim(self.gaussian_params_)
  1811. def _check_loaded_settings(self, data):
  1812. """Check if settings added, and update the object as needed.
  1813. Parameters
  1814. ----------
  1815. data : dict
  1816. A dictionary of data that has been added to the object.
  1817. """
  1818. # If settings not loaded from file, clear from object, so that default
  1819. # settings, which are potentially wrong for loaded data, aren't kept
  1820. if not set(OBJ_DESC['settings']).issubset(set(data.keys())):
  1821. # Reset all public settings to None
  1822. for setting in OBJ_DESC['settings']:
  1823. setattr(self, setting, None)
  1824. # If aperiodic params available, infer whether knee fitting was used,
  1825. if not np.all(np.isnan(self.aperiodic_params_)):
  1826. self.aperiodic_mode = infer_ap_func(self.aperiodic_params_)
  1827. # Reset internal settings so that they are consistent with what was loaded
  1828. # Note that this will set internal settings to None, if public settings unavailable
  1829. self._reset_internal_settings()
  1830. def _regenerate_freqs(self):
  1831. """Regenerate the frequency vector, given the object metadata."""
  1832. self.freqs = gen_freqs(self.freq_range, self.freq_res)
  1833. def _regenerate_model(self):
  1834. """Regenerate model fit from parameters."""
  1835. self.fooofed_spectrum_, self._peak_fit, self._ap_fit = gen_model(
  1836. self.freqs, self.aperiodic_params_, self.gaussian_params_, return_components=True)

fit.py at commit 8c35e00, no license · at the source

Overview

Authors: Gustav Kühn1, Hussin El Rashidy1,2, Federico Calegari1, Shonali Dhingra1
  1. Center for Regenerative Therapies TU Dresden, Technische Universität Dresden, Dresden 01307, Germany
  2. Faculty of Computer Sciences, Technische Universität Dresden, Dresden 01062, Germany
Journal: eNeuro, volume 13, issue 3, pages ENEURO.0136-25.2026
Dates: received 27 March 2025; accepted 18 February 2026; published online 25 March 2026; in print March 2026
Type: Research article · Language: English
License: CC BY
Identifiers: DOI 10.1523/eneuro.0136-25.2026 · PMID 41819818 · PMCID PMC13064425 · OpenAlex W7135062485
Open access: gold, a free copy (OpenAlex)
Status: code verified (how the code is found and verified)
Categories: EEG (modality), extracellular electrophysiology (units, LFP) (modality), mouse (organism)
Methods: Spectral & time-frequency, Connectivity, Physiology & signal measures
Keywords: aperiodicity, CA1, dentate gyrus, electrophysiology, hippocampus, local field potential
MeSH: Brain Waves*, CA1 Region, Hippocampal*, Dentate Gyrus*, Animals, Computer Simulation, Electroencephalography, Male, Mice, Mice, Inbred C57BL, Signal Processing, Computer-Assisted (* major topic)
Journal subjects: Research Article: New Research, Novel Tools and Methods
Topic: Neural dynamics and brain function (Cognitive Neuroscience, Neuroscience), according to OpenAlex
Funding: Deutsche Forschungsgemeinschaft (CA893/17-1); Volkswagen Foundation (Change of Course)
Citations: not cited yet (Europe PMC); 45 references in the paper

Abstract

Rodent hippocampal power spectra comprise periodic and aperiodic components. The periodic components (brain rhythms) contain information about the behavioral or cognitive state of the animal. The aperiodic components are rarely studied, and their functionality is not well understood, though they have shown to be correlated with animal's age or the excitation–inhibition ratio of the brain region. To study these components in the mouse hippocampus, we modified the existing open-source FOOOF toolbox, which was originally optimized for EEG data. First, using simulated data, we show that our modifications decrease the error in assessment of the low frequency periodic components from 3 to 0.1%. Second, using tetrode electrophysiological signals from adult males, we compare the aperiodic activity within mice hippocampal subregions, CA1 and dentate gyrus (DG). Our optimization of FOOOF improved the aperiodic assessment errors by ∼50% and were critical in making the first assessment of the aperiodic components in these brain regions. Our results show significantly larger aperiodic exponents and knee frequencies in the DG than in CA1, which have been proposed to correlate with lower excitation and shorter timescales. Our work highlights the subtle differences in electrophysiology field potentials between hippocampal subregions and presents the improvements needed in the existing open-source toolbox to be able to see such differences.

Reproduced under the paper's license (CC BY), from the paper cited above.

Repository

Its files are read in the Code ↔ Paper reader above, with 6 matches between paragraphs and lines of code.

hurashidy/lfp_fooof

License: none: the authors keep all their rights
State: the link answers, verified on 30 September 2026
Evidence: files inventoried
Commit: 8c35e002b5db76d15d3cbabc22d7c5763ec47f14, 18 July 2026
Languages: Python (2), MATLAB (2), Jupyter (1)
Size: 8 files, 5 scripts
Software Heritage: not archived
Found in: “Code accessibility”
Holds: README, 1 notebook
Not found: license file, CITATION.cff, environment file, tests, continuous integration, documentation
Tools: NumPy (3 files), specparam (formerly FOOOF) (3 files), Matplotlib (2 files), SciPy (2 files), Signal Processing Toolbox (1 file), statsmodels (1 file)
Availability: 1 check, the latest on 30 September 2026: the link answers
  • 30 September 2026: the link answers
6 files

Code accessibility

The code/software described in the paper is freely available online at (https://github.com/huRashidy/LFP_FOOOF/tree/main) and as Extended Data. We successfully implemented these codes on multiple computers using Windows operating system.

Reproduced under the paper's license (CC BY), from the paper cited above.

Tracing map

Proposed by the machine: these links were found in the paper and verified at the source, without human review. The map will receive a Zenodo DOI once one of the paper's authors has validated it with their ORCID.

What the map holds:

  • 1 repository of the authors' code, each at its verified commit, with its license and how the link was found in the paper;
  • 5 scripts, each with its path and the digest of its content;
  • 6 matches between paragraphs of the paper and lines of the code (method lexical-v1);
  • neither the text of the paper nor the code itself.

Its JSON (tracing-map.json) is deposited on Zenodo with its DOI once the map is validated.

Data

No dataset and no data link were found in the paper.

Versions

The history of this record: each version stored by the harvester or made by a correction of its authors or of the maintainers of its code, and what changed in its facts. The texts of the paper (its abstract, its availability statements) are not part of it; versions that changed only those are not listed.

Version 1, 30 September 2026: the first record

Recorded: type, language, journal, volume, issue, pages, dates, 4 authors, 6 keywords, 10 MeSH terms, 2 funders, 43 references.

Cite

This paper

Kühn, G., Rashidy, H. E., Calegari, F., & Dhingra, S. (2026). Aperiodicity in Mouse CA1 and DG Power Spectra. eNeuro, 13(3), ENEURO.0136-25.2026. https://doi.org/10.1523/eneuro.0136-25.2026

BibTeX

@article{kuhn2026aperiodicity,
author = {Kühn, Gustav and Rashidy, Hussin El and Calegari, Federico and Dhingra, Shonali},
title = {{Aperiodicity in Mouse CA1 and DG Power Spectra}},
journal = {eNeuro},
year = {2026},
month = mar,
volume = {13},
number = {3},
pages = {ENEURO.0136--25.2026},
publisher = {Society for Neuroscience},
issn = {2373-2822},
doi = {10.1523/eneuro.0136-25.2026},
url = {https://doi.org/10.1523/eneuro.0136-25.2026},
pmid = {41819818},
pmcid = {PMC13064425}
}

RIS

TY - JOUR
AU - Kühn, Gustav
AU - Rashidy, Hussin El
AU - Calegari, Federico
AU - Dhingra, Shonali
TI - Aperiodicity in Mouse CA1 and DG Power Spectra
T2 - eNeuro
J2 - eNeuro
PY - 2026
DA - 2026/03/27
VL - 13
IS - 3
SP - ENEURO.0136
EP - 25.2026
SN - 2373-2822
PB - Society for Neuroscience
DO - 10.1523/eneuro.0136-25.2026
UR - https://doi.org/10.1523/eneuro.0136-25.2026
LA - en
ER -

CSL-JSON

{
"id": "10.1523/eneuro.0136-25.2026",
"type": "article-journal",
"title": "Aperiodicity in Mouse CA1 and DG Power Spectra",
"container-title": "eNeuro",
"author": [
{
"family": "Kühn",
"given": "Gustav"
},
{
"family": "Rashidy",
"given": "Hussin El"
},
{
"family": "Calegari",
"given": "Federico"
},
{
"family": "Dhingra",
"given": "Shonali"
}
],
"container-title-short": "eNeuro",
"volume": "13",
"issue": "3",
"page": "ENEURO.0136-25.2026",
"DOI": "10.1523/eneuro.0136-25.2026",
"PMID": "41819818",
"PMCID": "PMC13064425",
"ISSN": "2373-2822",
"publisher": "Society for Neuroscience",
"URL": "https://doi.org/10.1523/eneuro.0136-25.2026",
"language": "en",
"issued": {
"date-parts": [
[
2026,
3,
27
]
]
}
}

The tracing map gets a citation of its own once an author has validated it and it has a DOI.

Similar papers

The papers with a page that share the most with this one: the tools found in their code, their categories, datasets, cited references and authors, the rarest counting most.

[1] doi:10.1111/ejn.15829 [code]
Robust estimation of 1/f activity improves oscillatory burst detection.
Journal: The European journal of neuroscience
In common: specparam (formerly FOOOF), Signal Processing Toolbox, SciPy, 2 other tools, 10 references
[2] doi:10.1093/cercor/bhae186 [code]
Aperiodic components of local field potentials reflect inherent differences between cortical and subcortical activity.
Journal: Cerebral cortex (New York, N.Y. : 1991)
In common: specparam (formerly FOOOF), statsmodels, Signal Processing Toolbox, 2 other tools, extracellular electrophysiology (units, LFP), EEG, 8 references
[3] doi:10.1038/s41398-023-02634-9 [code]
Clinical EEG slowing induced by electroconvulsive therapy is better described by increased frontal aperiodic activity.
Journal: Translational psychiatry
In common: specparam (formerly FOOOF), statsmodels, SciPy, 2 other tools, EEG, 6 references
[4] doi:10.1038/s42003-023-05168-4 [code]
Comparative analysis of multifaceted neural effects associated with varying endogenous cognitive load.
Journal: Communications biology
In common: specparam (formerly FOOOF), Signal Processing Toolbox, SciPy, 2 other tools, EEG, 5 references
[5] doi:10.7554/elife.101071 [code]
Modulation of aperiodic EEG activity provides sensitive index of cognitive state changes during working memory task.
Journal: eLife
In common: specparam (formerly FOOOF), SciPy, Matplotlib, 1 other tool, EEG, 6 references
[6] doi:10.1162/imag.a.150 [code]
Task-related changes in aperiodic activity are related to visual working memory capacity independent of event-related potentials and alpha oscillations.
Journal: Imaging neuroscience (Cambridge, Mass.)
In common: EEG, 9 references
[7] doi:10.1371/journal.pcbi.1012794 [code]
Contributions of action potentials to scalp EEG: Theory and biophysical simulations.
Journal: PLoS computational biology
In common: Signal Processing Toolbox, SciPy, Matplotlib, 1 other tool, EEG, 7 references
[8] doi:10.1093/cercor/bhad482 [code]
Test-retest reliability of spectral parameterization by 1/f characterization using SpecParam.
Journal: Cerebral cortex (New York, N.Y. : 1991)
In common: specparam (formerly FOOOF), statsmodels, SciPy, 2 other tools, EEG, 5 references
[9] doi:10.1523/jneurosci.0265-23.2023 [code]
Human Sensorimotor Beta Event Characteristics and Aperiodic Signal Are Highly Heritable.
Journal: The Journal of neuroscience : the official journal of the Society for Neuroscience
In common: specparam (formerly FOOOF), SciPy, Matplotlib, 1 other tool, 6 references
[10] doi:10.1186/s12916-025-04447-7 [code]
Dynamic excitation/inhibition balance preceding seizure onset and its link to functional and structural brain architecture.
Journal: BMC medicine
In common: specparam (formerly FOOOF), statsmodels, SciPy, 2 other tools, EEG, 5 references

Contribute

The authors of this paper can claim it, correct its record and validate its tracing map, and the maintainers of its code (its owner, or a public member of its organization) correct what it says of their repository; anyone signed in can ask for its removal. Every request goes to OSCR's own machine, which answers it; your account page follows them.

Sign in with ORCID to claim this paper as one of its authors, correct its record or validate its tracing map: when the paper's metadata lists your ORCID iD, you are recognized at once. Maintainers of its code: sign in with GitHub, then claim the repository on your account page.

Add the badge to the code's README

OSCR: the badge of this paper

If you are an author or a maintainer of this paper's code, you can add this badge. It says what OSCR knows of the paper (its code, its validated tracing map) and links the code to this page. Paste one of these snippets under the README's title. Keep the two marks around it, so that the badge can be replaced in place and is never added twice. OSCR changes nothing in your repository.

Markdown
<!-- oscr-badge:start -->
[![OSCR](https://openscicode.org/badge/doi/10.1523/eneuro.0136-25.2026.svg)](https://openscicode.org/badge/doi/10.1523/eneuro.0136-25.2026)
<!-- oscr-badge:end -->
reStructuredText
.. oscr-badge:start

.. image:: https://openscicode.org/badge/doi/10.1523/eneuro.0136-25.2026.svg
   :target: https://openscicode.org/badge/doi/10.1523/eneuro.0136-25.2026
   :alt: OSCR

.. oscr-badge:end
HTML
<!-- oscr-badge:start -->
<a href="https://openscicode.org/badge/doi/10.1523/eneuro.0136-25.2026"><img src="https://openscicode.org/badge/doi/10.1523/eneuro.0136-25.2026.svg" alt="OSCR"></a>
<!-- oscr-badge:end -->

On GitHub, open the README, choose Edit (the pencil), paste the snippet under the title, then "Commit changes".

If you choose to, you can have the badge kept up to date for you, with oscr badge --write from the command line or with the GitHub Action in your repository's workflows. Every other form and option (a card, AsciiDoc, LaTeX, shields.io) is on the badge page.

Request its removal

To ask OSCR to remove this record, the copies of its authors' scripts or its tracing map, use the removal request page: signed in, you say who you are, what to remove and why, then review and confirm the request. Published rules decide every request (how).

Discussion, reproductions, activity

Discussion: code errors and mismatches between the code and the paper about this paper and its code, from readers and its authors, beside the paper and its tracing map.

Report a mismatch between the code and the paper · Report a code error (signed in)

Reproductions: reports from readers who ran the authors' code and did not get the paper's result, what they ran, with which environment, commit and data.

Report a reproduction failure (signed in)

Versions of its code: the releases of its code tied to a version of this paper (the preprint, the accepted manuscript, the version of record…), each with the tracing map versioned with it.

Activity: what happens around this paper: new versions of its record, its map's validation, discussions and reproductions. It opens with sign-in.