bycycle-tools / bycycle

Cycle-by-cycle analysis of neural oscillations.

Home Page:https://bycycle-tools.github.io/

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Bycycle Group Analysis

ryanhammonds opened this issue · comments

Stephanie has suggested functionality to more easily facilitate an analysis for epoched event-related data. This would mean a implementation similar to FOOOFGroup, but for a group of time series. It may be useful to have an additional column in the pandas dataframe that indicates the trial type of each segment of the signal (row).

commented

This sound like a good idea, but just a note that I think there are perhaps two different things here:

  • 2d arrays of channels (multiple channels across time)
  • 2d or 3d arrays of channels (one or more channels, split up across time, into epochs)

By comparison to FOOOF, FOOOFGroup does 2d arrays of spectra, which is equivalent to multiple channels, but this is not epoched. Once there is functionality for 2D arrays, epochs can be run by a helper function to distribute each epoch (2d array) into the 2d functionality (in fooof fit_fooof_3d does this, applying FOOOFGroups to 2d arrays from across a 3d array input).

As I understand it, ByCycle right now takes in a single channel, so this idea can mean both generalizing ByCycle to run across multiple channels, and/or generalizing ByCycycle to run across multiple segments (epochs), for one or many channels.

The most useful approach for the moment would be a group analysis across epochs, since here the parameters for burst detection should be kept the same but burst detection should be performed simultaneously across the different epochs (e.g. considering a global amp_fraction).

Since channels have different SNR, they will require adjustment of burst detection parameters per channel and can be processed independently.

So currently, it is possible to do the latter by just calling compute_features in a loop, but the former is trickier at the moment, since burst detection will be performed independently for each epoch. Would focus on the epoch-based scenario for now. (& maybe convenience wrapper for channel-based at some later point in time)

This was addressed in #64 (bycycle.group.compute_features_2d and bycycle.group.compute_features_3d). These funcs allow flexible burst detection parameters across axes in either 2d or 3d arrays (i.e. one single set of params for all signals or unique sets of params across different axes/signals). The docstring examples demonstrate usage, but an additional tutorial may be useful. Lastly, these function have been parallelized using the multiprocessing module, which may be useful for large arrays. I'll leave this open until these funcs are confirmed stable / useful, maybe until the next release.

Here are additional examples of usage copied from the PR:

from bycycle.group import compute_features_3d
import numpy as np
from neurodsp.sim import sim_bursty_oscillation

# Simulate a 3d array of shape (n_channels, n_epochs, n_signals)
fs, f_range, n_chs, n_epochs = 500, (8, 12), 2, 2
sigs = np.array([[sim_bursty_oscillation(10, fs, 10) for epoch in range(n_epochs)] for ch in range(n_chs)])


# Apply the same thresholds across all channels and epochs
threshold_kwargs = {'amp_consistency_threshold': .5, 'period_consistency_threshold': .5,
                    'monotonicity_threshold': .8, 'min_n_cycles': 3}

features = compute_features_3d(sigs, fs, f_range, return_samples=False, n_jobs=2,
                               compute_features_kwargs={'threshold_kwargs': threshold_kwargs})


# Apply channel-specific thresholds
threshold_kwargs_ch1 = {'amp_consistency_threshold': .25, 'period_consistency_threshold': .25,
                        'monotonicity_threshold': .25, 'min_n_cycles': 3}

threshold_kwargs_ch2 = {'amp_consistency_threshold': .5, 'period_consistency_threshold': .5,
                        'monotonicity_threshold': .5, 'min_n_cycles': 3}

compute_kwargs = [{'threshold_kwargs': threshold_kwargs_ch1}, {'threshold_kwargs': threshold_kwargs_ch2}]

features = compute_features_3d(sigs, fs, f_range, return_samples=False, n_jobs=2,
                               compute_features_kwargs={'threshold_kwargs': compute_kwargs})


# Apply unique threshold for each channel and epoch
threshold_kwargs_ch1_epoch1 = {'threshold_kwargs': 
                                   {'amp_consistency_threshold': .2, 'period_consistency_threshold': .2,
                                    'monotonicity_threshold': .2}
                              }

threshold_kwargs_ch1_epoch2 = {'threshold_kwargs':  
                                   {'amp_consistency_threshold': .4, 'period_consistency_threshold': .4,
                                    'monotonicity_threshold': .4}
                              }

threshold_kwargs_ch2_epoch1 = {'threshold_kwargs': 
                                   {'amp_consistency_threshold': .6, 'period_consistency_threshold': .6,
                                   'monotonicity_threshold': .6}
                              }

threshold_kwargs_ch2_epoch2 = {'threshold_kwargs': 
                                   {'amp_consistency_threshold': .8, 'period_consistency_threshold': .8,
                                    'monotonicity_threshold': .8}
                              }

compute_kwargs = [[threshold_kwargs_ch1_epoch1, threshold_kwargs_ch1_epoch2],
                  [threshold_kwargs_ch2_epoch1, threshold_kwargs_ch2_epoch2]]

features = compute_features_3d(sigs, fs, f_range, return_samples=False, n_jobs=2,
                               compute_features_kwargs=compute_kwargs)