Skip to content

Commit

Permalink
Remove features dictionary as input to calc_feature (#349)
Browse files Browse the repository at this point in the history
Remove features dict as input to calc_feature
  • Loading branch information
toni-neurosc authored Jun 26, 2024
1 parent ece9500 commit 01bcf70
Show file tree
Hide file tree
Showing 16 changed files with 152 additions and 140 deletions.
13 changes: 8 additions & 5 deletions examples/plot_2_example_add_feature.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,18 +37,21 @@ def __init__(

self.feature_name = "channel_mean"

def calc_feature(self, data: np.ndarray, features_compute: dict) -> dict:
def calc_feature(self, data: np.ndarray) -> dict:
# First, create an empty dictionary to store the calculated features
feature_results = {}

# Here you can add any feature calculation code
# This example simply calculates the mean signal for each channel
ch_means = np.mean(data, axis=1)

# Store the calculated features in the features_compute dictionary
# Store the calculated features in the feature_results dictionary
# Be careful to use a unique keyfor each channel and metric you compute
for ch_idx, ch in enumerate(self.ch_names):
features_compute[f"{self.feature_name}_{ch}"] = ch_means[ch_idx]
feature_results[f"{self.feature_name}_{ch}"] = ch_means[ch_idx]

# Return the updated features_compute dictionary to the stream
return features_compute
# Return the updated feature_results dictionary to the stream
return feature_results


nm.add_custom_feature("channel_mean", ChannelMean)
Expand Down
11 changes: 6 additions & 5 deletions py_neuromodulation/nm_bispectra.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ def __init__(

# self.freqs: np.ndarray = np.array([]) # In case we pre-computed this

def calc_feature(self, data: np.ndarray, features_compute: dict) -> dict:
def calc_feature(self, data: np.ndarray) -> dict:
from pybispectra import compute_fft, WaveShape

# PyBispectra's compute_fft uses PQDM to parallelize the calculation per channel
Expand Down Expand Up @@ -120,7 +120,8 @@ def calc_feature(self, data: np.ndarray, features_compute: dict) -> dict:
f1s=tuple(self.settings.f1s), # type: ignore
f2s=tuple(self.settings.f2s), # type: ignore
)


feature_results = {}
for ch_idx, ch_name in enumerate(self.ch_names):

bispectrum = waveshape._bicoherence[ch_idx] # Same as waveshape.results._data, skips a copy
Expand All @@ -136,13 +137,13 @@ def calc_feature(self, data: np.ndarray, features_compute: dict) -> dict:
data_bs = spectrum_ch[range_, range_]

for bispectrum_feature in self.used_features:
features_compute[
feature_results[
f"{ch_name}_Bispectrum_{component}_{bispectrum_feature}_{fb}"
] = FEATURE_DICT[bispectrum_feature](data_bs)

if self.settings.compute_features_for_whole_fband_range:
features_compute[
feature_results[
f"{ch_name}_Bispectrum_{component}_{bispectrum_feature}_whole_fband_range"
] = FEATURE_DICT[bispectrum_feature](spectrum_ch)

return features_compute
return feature_results
27 changes: 14 additions & 13 deletions py_neuromodulation/nm_bursts.py
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ def __init__(
self.label_structure_matrix = np.zeros((3, 3, 3))
self.label_structure_matrix[1, 1, :] = 1

def calc_feature(self, data: np.ndarray, features_compute: dict) -> dict:
def calc_feature(self, data: np.ndarray) -> dict:
from scipy.signal import hilbert
from scipy.ndimage import label, sum_labels as label_sum, mean as label_mean

Expand Down Expand Up @@ -246,40 +246,41 @@ def calc_feature(self, data: np.ndarray, features_compute: dict) -> dict:
self.end_in_burst = bursts[:, :, -1] # End in burst

# Create dictionary of features which is the correct return format
feature_results = {}
for (ch_i, ch), (fb_i, fb), feat in self.feature_combinations:
self.STORE_FEAT_DICT[feat](features_compute, ch_i, ch, fb_i, fb)
self.STORE_FEAT_DICT[feat](feature_results, ch_i, ch, fb_i, fb)

return features_compute
return feature_results

def store_duration(
self, features_compute: dict, ch_i: int, ch: str, fb_i: int, fb: str
self, feature_results: dict, ch_i: int, ch: str, fb_i: int, fb: str
):
features_compute[f"{ch}_bursts_{fb}_duration_mean"] = self.burst_duration_mean[
feature_results[f"{ch}_bursts_{fb}_duration_mean"] = self.burst_duration_mean[
ch_i, fb_i
]

features_compute[f"{ch}_bursts_{fb}_duration_max"] = self.burst_duration_max[
feature_results[f"{ch}_bursts_{fb}_duration_max"] = self.burst_duration_max[
ch_i, fb_i
]

def store_amplitude(
self, features_compute: dict, ch_i: int, ch: str, fb_i: int, fb: str
self, feature_results: dict, ch_i: int, ch: str, fb_i: int, fb: str
):
features_compute[f"{ch}_bursts_{fb}_amplitude_mean"] = (
feature_results[f"{ch}_bursts_{fb}_amplitude_mean"] = (
self.burst_amplitude_mean[ch_i, fb_i]
)
features_compute[f"{ch}_bursts_{fb}_amplitude_max"] = self.burst_amplitude_max[
feature_results[f"{ch}_bursts_{fb}_amplitude_max"] = self.burst_amplitude_max[
ch_i, fb_i
]

def store_burst_rate(
self, features_compute: dict, ch_i: int, ch: str, fb_i: int, fb: str
self, feature_results: dict, ch_i: int, ch: str, fb_i: int, fb: str
):
features_compute[f"{ch}_bursts_{fb}_burst_rate_per_s"] = self.burst_rate_per_s[
feature_results[f"{ch}_bursts_{fb}_burst_rate_per_s"] = self.burst_rate_per_s[
ch_i, fb_i
]

def store_in_burst(
self, features_compute: dict, ch_i: int, ch: str, fb_i: int, fb: str
self, feature_results: dict, ch_i: int, ch: str, fb_i: int, fb: str
):
features_compute[f"{ch}_bursts_{fb}_in_burst"] = self.end_in_burst[ch_i, fb_i]
feature_results[f"{ch}_bursts_{fb}_in_burst"] = self.end_in_burst[ch_i, fb_i]
20 changes: 11 additions & 9 deletions py_neuromodulation/nm_coherence.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ def __init__(
self.coh_val = None
self.icoh_val = None

def get_coh(self, features_compute, x, y):
def get_coh(self, feature_results, x, y):
from scipy.signal import welch, csd

self.f, self.Pxx = welch(x, self.sfreq, self.window, nperseg=128)
Expand Down Expand Up @@ -104,7 +104,7 @@ def get_coh(self, features_compute, x, y):
self.fband_names[idx],
]
)
features_compute[feature_name] = feature_calc
feature_results[feature_name] = feature_calc
if self.features_coh.max_fband:
feature_calc = np.max(
coh_val[np.bitwise_and(self.f > fband[0], self.f < fband[1])]
Expand All @@ -119,7 +119,7 @@ def get_coh(self, features_compute, x, y):
self.fband_names[idx],
]
)
features_compute[feature_name] = feature_calc
feature_results[feature_name] = feature_calc
if self.features_coh.max_allfbands:
feature_calc = self.f[np.argmax(coh_val)]
feature_name = "_".join(
Expand All @@ -132,8 +132,8 @@ def get_coh(self, features_compute, x, y):
self.fband_names[idx],
]
)
features_compute[feature_name] = feature_calc
return features_compute
feature_results[feature_name] = feature_calc
return feature_results


class NMCoherence(NMFeature):
Expand Down Expand Up @@ -235,12 +235,14 @@ def test_settings(
"feature coherence enabled, but no coherence['method'] selected"
)

def calc_feature(self, data: np.ndarray, features_compute: dict) -> dict:
def calc_feature(self, data: np.ndarray) -> dict:
feature_results = {}

for coh_obj in self.coherence_objects:
features_compute = coh_obj.get_coh(
features_compute,
feature_results = coh_obj.get_coh(
feature_results,
data[coh_obj.ch_1_idx, :],
data[coh_obj.ch_2_idx, :],
)

return features_compute
return feature_results
46 changes: 21 additions & 25 deletions py_neuromodulation/nm_features.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
from typing import Protocol, Type, runtime_checkable, TYPE_CHECKING, TypeVar
from collections.abc import Sequence
import numpy as np

if TYPE_CHECKING:
import numpy as np
from nm_settings import NMSettings

from py_neuromodulation.nm_types import ImportDetails, get_class, FeatureName
Expand All @@ -14,19 +14,19 @@ def __init__(
self, settings: "NMSettings", ch_names: Sequence[str], sfreq: int | float
) -> None: ...

def calc_feature(self, data: "np.ndarray", features_compute: dict) -> dict:
def calc_feature(self, data: np.ndarray) -> dict:
"""
Feature calculation method. Each method needs to loop through all channels
Feature calculation method. Each method needs to loop through all channels
Parameters
----------
data : 'np.ndarray'
(channels, time)
features_compute : dict
Returns
-------
dict
----------
data : 'np.ndarray'
(channels, time)
feature_results : dict
Returns
-------
dict
"""
...

Expand All @@ -50,18 +50,17 @@ def calc_feature(self, data: "np.ndarray", features_compute: dict) -> dict:


class FeatureProcessors:
"""Class for calculating features.p"""
"""Class for storing NMFeature objects and calculating features during processing"""

def __init__(
self, settings: "NMSettings", ch_names: list[str], sfreq: float
) -> None:
"""_summary_
"""Initialize FeatureProcessors object with settings, channel names and sampling frequency.
Parameters
----------
settings : nm_settings.NMSettings
ch_names : list[str]
sfreq : float
Args:
settings (NMSettings): PyNM settings object
ch_names (list[str]): list of channel names
sfreq (float): sampling frequency in Hz
"""
from py_neuromodulation import user_features

Expand All @@ -86,7 +85,7 @@ def register_new_feature(self, feature_name: str, feature: NMFeature) -> None:
"""
self.features[feature_name] = feature # type: ignore

def estimate_features(self, data: "np.ndarray") -> dict:
def estimate_features(self, data: np.ndarray) -> dict:
"""Calculate features, as defined in settings.json
Features are based on bandpower, raw Hjorth parameters and sharp wave
characteristics.
Expand All @@ -100,15 +99,12 @@ def estimate_features(self, data: "np.ndarray") -> dict:
dat (dict): naming convention : channel_method_feature_(f_band)
"""

features_compute: dict = {}
feature_results: dict = {}

for feature in self.features.values():
features_compute = feature.calc_feature(
data,
features_compute,
)
feature_results.update(feature.calc_feature(data))

return features_compute
return feature_results

def get_feature(self, fname: FeatureName) -> NMFeature:
return self.features[fname]
Expand Down
22 changes: 10 additions & 12 deletions py_neuromodulation/nm_fooof.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from collections.abc import Iterable
from pyexpat import features
import numpy as np

from typing import TYPE_CHECKING
Expand Down Expand Up @@ -79,13 +80,9 @@ def __init__(
verbose=False,
)

def calc_feature(
self,
data: np.ndarray,
features_compute: dict,
) -> dict:
def calc_feature(self, data: np.ndarray) -> dict:
from scipy.fft import rfft

spectra = np.abs(rfft(data[:, -self.num_samples :])) # type: ignore

self.fm.fit(self.f_vec, spectra, self.settings.freq_range_hz)
Expand All @@ -94,7 +91,8 @@ def calc_feature(
raise RuntimeError("FOOOF failed to fit model to data.")

failed_fits: list[int] = self.fm.null_inds_


feature_results = {}
for ch_idx, ch_name in enumerate(self.ch_names):
FIT_PASSED = ch_idx not in failed_fits
exp = self.fm.get_params("aperiodic_params", "exponent")[ch_idx]
Expand All @@ -103,10 +101,10 @@ def calc_feature(
f_name = f"{ch_name}_fooof_a_{self.feat_name_map[feat]}"

if not FIT_PASSED:
features_compute[f_name] = None
feature_results[f_name] = None

elif feat == "knee" and exp == 0:
features_compute[f_name] = None
feature_results[f_name] = None

else:
params = self.fm.get_params("aperiodic_params", feat)[ch_idx]
Expand All @@ -117,7 +115,7 @@ def calc_feature(
else:
params = params ** (1 / exp)

features_compute[f_name] = np.nan_to_num(params)
feature_results[f_name] = np.nan_to_num(params)

peaks_dict: dict[str, np.ndarray | None] = {
"bw": self.fm.get_params("peak_params", "BW") if FIT_PASSED else None,
Expand All @@ -134,10 +132,10 @@ def calc_feature(
for feat in self.settings.periodic.get_enabled():
f_name = f"{ch_name}_fooof_p_{peak_idx}_{self.feat_name_map[feat]}"

features_compute[f_name] = (
feature_results[f_name] = (
peaks_dict[self.feat_name_map[feat]][peak_idx]
if peak_idx < len(peaks_dict[self.feat_name_map[feat]])
else None
)

return features_compute
return feature_results
24 changes: 12 additions & 12 deletions py_neuromodulation/nm_hjorth_raw.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
DOI: 10.1016/0013-4694(70)90143-4
"""

from pyexpat import features
import numpy as np
from collections.abc import Iterable

Expand All @@ -18,7 +19,7 @@ def __init__(
) -> None:
self.ch_names = ch_names

def calc_feature(self, data: np.ndarray, features_compute: dict) -> dict:
def calc_feature(self, data: np.ndarray) -> dict:
var = np.var(data, axis=-1)
deriv1 = np.diff(data, axis=-1)
deriv2 = np.diff(deriv1, axis=-1)
Expand All @@ -30,24 +31,23 @@ def calc_feature(self, data: np.ndarray, features_compute: dict) -> dict:
mobility = np.nan_to_num(np.sqrt(deriv1_var / var))
complexity = np.nan_to_num(deriv1_mobility / mobility)

feature_results = {}
for ch_idx, ch_name in enumerate(self.ch_names):
features_compute[f"{ch_name}_RawHjorth_Activity"] = activity[ch_idx]
features_compute[f"{ch_name}_RawHjorth_Mobility"] = mobility[ch_idx]
features_compute[f"{ch_name}_RawHjorth_Complexity"] = complexity[ch_idx]
feature_results[f"{ch_name}_RawHjorth_Activity"] = activity[ch_idx]
feature_results[f"{ch_name}_RawHjorth_Mobility"] = mobility[ch_idx]
feature_results[f"{ch_name}_RawHjorth_Complexity"] = complexity[ch_idx]

return features_compute
return feature_results


class Raw(NMFeature):
def __init__(self, settings: dict, ch_names: Iterable[str], sfreq: float) -> None:
self.ch_names = ch_names

def calc_feature(
self,
data: np.ndarray,
features_compute: dict,
) -> dict:
def calc_feature(self, data: np.ndarray) -> dict:
feature_results = {}

for ch_idx, ch_name in enumerate(self.ch_names):
features_compute["_".join([ch_name, "raw"])] = data[ch_idx, -1]
feature_results["_".join([ch_name, "raw"])] = data[ch_idx, -1]

return features_compute
return feature_results
Loading

0 comments on commit 01bcf70

Please sign in to comment.