Source code for features.nolds

import numpy as np
from collections.abc import Iterable

from typing import TYPE_CHECKING

from py_neuromodulation.utils.types import NMFeature, BoolSelector, NMBaseModel

from pydantic import field_validator

if TYPE_CHECKING:
    from py_neuromodulation import NMSettings


class NoldsFeatures(BoolSelector):
    sample_entropy: bool = False
    correlation_dimension: bool = False
    lyapunov_exponent: bool = True
    hurst_exponent: bool = False
    detrended_fluctuation_analysis: bool = False


class NoldsSettings(NMBaseModel):
    raw: bool = True
    frequency_bands: list[str] = ["low_beta"]
    features: NoldsFeatures = NoldsFeatures()

    @field_validator("frequency_bands")
    def fbands_spaces_to_underscores(cls, frequency_bands):
        return [f.replace(" ", "_") for f in frequency_bands]


[docs] class Nolds(NMFeature): def __init__( self, settings: "NMSettings", ch_names: Iterable[str], sfreq: float ) -> None: self.settings = settings.nolds_settings self.ch_names = ch_names if len(self.settings.frequency_bands) > 0: from py_neuromodulation.features.bandpower import BandPower self.bp_filter = BandPower(settings, ch_names, sfreq, use_kf=False) # Check if the selected frequency bands are defined in the global settings for fb in settings.nolds_settings.frequency_bands: assert ( fb in settings.frequency_ranges_hz ), f"{fb} selected in nolds_features, but not defined in s['frequency_ranges_hz']"
[docs] def calc_feature(self, data: np.ndarray) -> dict: feature_results = {} data = np.nan_to_num(data) if self.settings.raw: feature_results = self.calc_nolds(data, feature_results) if len(self.settings.frequency_bands) > 0: data_filt = self.bp_filter.bandpass_filter.filter_data(data) for f_band_idx, f_band in enumerate(self.settings.frequency_bands): # filter data now for a specific fband and pass to calc_nolds feature_results = self.calc_nolds( data_filt[:, f_band_idx, :], feature_results, f_band ) # ch, bands, samples return feature_results
def calc_nolds( self, data: np.ndarray, feature_results: dict, data_str: str = "raw" ) -> dict: for ch_idx, ch_name in enumerate(self.ch_names): for f_name in self.settings.features.get_enabled(): feature_results[f"{ch_name}_nolds_{f_name}_{data_str}"] = ( self.calc_nolds_feature(f_name, data[ch_idx, :]) if data[ch_idx, :].sum() else 0 ) return feature_results @staticmethod def calc_nolds_feature(f_name: str, dat: np.ndarray): import nolds match f_name: case "sample_entropy": return nolds.sampen(dat) case "correlation_dimension": return nolds.corr_dim(dat, emb_dim=2) case "lyapunov_exponent": return nolds.lyap_r(dat) case "hurst_exponent": return nolds.hurst_rs(dat) case "detrended_fluctuation_analysis": return nolds.dfa(dat) case _: raise ValueError(f"Invalid nolds feature name: {f_name}")