def test_amp(): """Test phase time series functionality""" # Load signal signal = np.load(data_path + 'sim_bursting.npy') Fs = 1000 # Sampling rate f_range = (6, 14) # Frequency range # Test output same length as input amp = filt.amp_by_time(signal, Fs, f_range, filter_kwargs={'N_seconds': .5}) assert len(signal) == len(amp) # Test results are the same if add NaNs to the side signal_nan = np.pad(signal, 10, mode='constant', constant_values=(np.nan,)) amp_nan = filt.amp_by_time(signal_nan, Fs, f_range, filter_kwargs={'N_seconds': .5}) np.testing.assert_allclose(amp_nan[10:-10], amp) # Test NaN is in same places as filtered signal signal_filt = filt.bandpass_filter(signal, Fs, (6, 14), N_seconds=.5) assert np.all(np.logical_not( np.logical_xor(np.isnan(amp), np.isnan(signal_filt)))) # Test works fine if input signal already has NaN signal_low = filt.lowpass_filter(signal, Fs, 30, N_seconds=.3) amp = filt.amp_by_time(signal_low, Fs, f_range, filter_kwargs={'N_seconds': .5}) assert len(signal) == len(amp) # Test option to not remove edge artifacts amp = filt.amp_by_time(signal, Fs, f_range, filter_kwargs={'N_seconds': .5}, remove_edge_artifacts=False) assert np.all(np.logical_not(np.isnan(amp)))
def twothresh_amp(x, Fs, f_range, amp_threshes, N_cycles_min=3, magnitude_type='amplitude', return_amplitude=False, filter_kwargs=None): """ Detect periods of oscillatory bursting in a neural signal by using two amplitude thresholds. Parameters ---------- x : 1d array voltage time series Fs : float sampling rate, Hz f_range : tuple of (float float) frequency range (Hz) for oscillator of interest amp_threshes : tuple of (float float) Threshold values for determining timing of bursts. These values are in units of amplitude (or power, if specified) normalized to the median amplitude (value 1). N_cycles_min : float minimum burst duration in terms of number of cycles of f_range[0] magnitude_type : {'power', 'amplitude'} metric of magnitude used for thresholding return_amplitude : bool if True, return the amplitude time series as an additional output filter_kwargs : dict keyword arguments to filt.bandpass_filter Returns ------- isosc_noshort : 1d array, type=bool array of same length as `x` indicating the parts of the signal for which the oscillation was detected """ # Set default filtering parameters if filter_kwargs is None: filter_kwargs = {} # Assure the amp_threshes is a tuple of length 2 if len(amp_threshes) != 2: raise ValueError( "Invalid number of elements in 'amp_threshes' parameter") # Compute amplitude time series x_amplitude = amp_by_time(x, Fs, f_range, filter_kwargs=filter_kwargs, remove_edge_artifacts=False) # Set magnitude as power or amplitude if magnitude_type == 'power': x_magnitude = x_amplitude**2 elif magnitude_type == 'amplitude': x_magnitude = x_amplitude else: raise ValueError("Invalid 'magnitude' parameter") # Rescale magnitude by median x_magnitude = x_magnitude / np.median(x_magnitude) # Identify time periods of oscillation using the 2 thresholds isosc = _2threshold_split(x_magnitude, amp_threshes[1], amp_threshes[0]) # Remove short time periods of oscillation min_period_length = int(np.ceil(N_cycles_min * Fs / f_range[0])) isosc_noshort = _rmv_short_periods(isosc, min_period_length) if return_amplitude: return isosc_noshort, x_magnitude else: return isosc_noshort
def compute_features(x, Fs, f_range, center_extrema='P', burst_detection_method='cycles', burst_detection_kwargs=None, find_extrema_kwargs=None, hilbert_increase_N=False): """ Segment a recording into individual cycles and compute features for each cycle Parameters ---------- x : 1d array Voltage time series. Fs : float Sampling rate (Hz). f_range : tuple of (float, float) Frequency range for narrowband signal of interest (Hz). center_extrema : {'P', 'T'} The center extrema in the cycle 'P' : cycles are defined trough-to-trough 'T' : cycles are defined peak-to-peak burst_detection_method : {'consistency', 'amp'} Method for detecting bursts. .. 'consistency': detect bursts based on the consistency of consecutive periods & amplitudes 'amp': detect bursts using an amplitude threshold burst_detection_kwargs : dict | None Keyword arguments for function to find label cycles as in or not in an oscillation. find_extrema_kwargs : dict | None Keyword arguments for function to find peaks an troughs (:func:`~.find_extrema`) to change filter Parameters or boundary.By default, it sets the filter length to three cycles of the low cutoff frequency (`f_range[0]`). hilbert_increase_N : bool Corresponding kwarg for :func:`~.amp_by_time` If true, this zeropads the signal when computing the Fourier transform, which can be necessary for computing it in a reasonable amount of time. Returns ------- df : pandas.DataFrame dataframe containing several features and identifiers for each cycle. Each row is one cycle. Columns (listed for peak-centered cycles): - ``sample_peak`` : sample of 'x' at which the peak occurs - ``sample_zerox_decay`` : sample of the decaying zerocrossing - ``sample_zerox_rise`` : sample of the rising zerocrossing - ``sample_last_trough`` : sample of the last trough - ``sample_next_trough`` : sample of the next trough - ``period`` : period of the cycle - ``time_decay`` : time between peak and next trough - ``time_rise`` : time between peak and previous trough - ``time_peak`` : time between rise and decay zerocrosses - ``time_trough`` : duration of previous trough estimated by zerocrossings - ``volt_decay`` : voltage change between peak and next trough - ``volt_rise`` : voltage change between peak and previous trough - ``volt_amp`` : average of rise and decay voltage - ``volt_peak`` : voltage at the peak - ``volt_trough`` : voltage at the last trough - ``time_rdsym`` : fraction of cycle in the rise period - ``time_ptsym`` : fraction of cycle in the peak period - ``band_amp`` : average analytic amplitude of the oscillation computed using narrowband filtering and the Hilbert transform. Filter length is 3 cycles of the low cutoff frequency. Average taken across all time points in the cycle. - ``is_burst`` : True if the cycle is part of a detected oscillatory burst - ``amp_fraction`` : normalized amplitude - ``amp_consistency`` : difference in the rise and decay voltage within a cycle - ``period_consistency`` : difference between a cycle’s period and the period of the adjacent cycles - ``monotonicity`` : fraction of instantaneous voltage changes between consecutive samples that are positive during the rise phase and negative during the decay phase Notes ----- Peak vs trough centering - By default, the first extrema analyzed will be a peak, and the final one a trough. In order to switch the preference, the signal is simply inverted and columns are renamed. - Columns are slightly different depending on if 'center_extrema' is set to 'P' or 'T'. """ # Set defaults if user input is None if burst_detection_kwargs is None: burst_detection_kwargs = {} warnings.warn(''' No burst detection parameters are provided. This is very much not recommended. Please inspect your data and choose appropriate parameters for "burst_detection_kwargs". Default burst detection parameters are likely not well suited for your desired application. ''') if find_extrema_kwargs is None: find_extrema_kwargs = {'filter_kwargs': {'N_cycles': 3}} else: # Raise warning if switch from peak start to trough start if 'first_extrema' in find_extrema_kwargs.keys(): raise ValueError('''This function has been designed to assume that the first extrema identified will be a peak. This cannot be overwritten at this time.''') # Negate signal if to analyze trough-centered cycles if center_extrema == 'P': pass elif center_extrema == 'T': x = -x else: raise ValueError( 'Parameter "center_extrema" must be either "P" or "T"') # Find peak and trough locations in the signal Ps, Ts = find_extrema(x, Fs, f_range, **find_extrema_kwargs) # Find zero-crossings zeroxR, zeroxD = find_zerox(x, Ps, Ts) # For each cycle, identify the sample of each extrema and zerocrossing shape_features = {} shape_features['sample_peak'] = Ps[1:] shape_features['sample_zerox_decay'] = zeroxD[1:] shape_features['sample_zerox_rise'] = zeroxR shape_features['sample_last_trough'] = Ts[:-1] shape_features['sample_next_trough'] = Ts[1:] # Compute duration of period shape_features['period'] = shape_features['sample_next_trough'] - \ shape_features['sample_last_trough'] # Compute duration of peak shape_features['time_peak'] = shape_features['sample_zerox_decay'] - \ shape_features['sample_zerox_rise'] # Compute duration of last trough shape_features['time_trough'] = zeroxR - zeroxD[:-1] # Determine extrema voltage shape_features['volt_peak'] = x[Ps[1:]] shape_features['volt_trough'] = x[Ts[:-1]] # Determine rise and decay characteristics shape_features['time_decay'] = (Ts[1:] - Ps[1:]) shape_features['time_rise'] = (Ps[1:] - Ts[:-1]) shape_features['volt_decay'] = x[Ps[1:]] - x[Ts[1:]] shape_features['volt_rise'] = x[Ps[1:]] - x[Ts[:-1]] shape_features['volt_amp'] = (shape_features['volt_decay'] + shape_features['volt_rise']) / 2 # Comptue rise-decay symmetry features shape_features[ 'time_rdsym'] = shape_features['time_rise'] / shape_features['period'] # Compute peak-trough symmetry features shape_features['time_ptsym'] = shape_features['time_peak'] / ( shape_features['time_peak'] + shape_features['time_trough']) # Compute average oscillatory amplitude estimate during cycle amp = amp_by_time(x, Fs, f_range, hilbert_increase_N=hilbert_increase_N, filter_kwargs={'N_cycles': 3}) shape_features['band_amp'] = [ np.mean(amp[Ts[i]:Ts[i + 1]]) for i in range(len(shape_features['sample_peak'])) ] # Convert feature dictionary into a DataFrame df = pd.DataFrame.from_dict(shape_features) # Define whether or not each cycle is part of a burst if burst_detection_method == 'cycles': df = detect_bursts_cycles(df, x, **burst_detection_kwargs) elif burst_detection_method == 'amp': df = detect_bursts_df_amp(df, x, Fs, f_range, **burst_detection_kwargs) else: raise ValueError('Invalid entry for "burst_detection_method"') # Rename columns if they are actually trough-centered if center_extrema == 'T': rename_dict = { 'sample_peak': 'sample_trough', 'sample_zerox_decay': 'sample_zerox_rise', 'sample_zerox_rise': 'sample_zerox_decay', 'sample_last_trough': 'sample_last_peak', 'sample_next_trough': 'sample_next_peak', 'time_peak': 'time_trough', 'time_trough': 'time_peak', 'volt_peak': 'volt_trough', 'volt_trough': 'volt_peak', 'time_rise': 'time_decay', 'time_decay': 'time_rise', 'volt_rise': 'volt_decay', 'volt_decay': 'volt_rise' } df.rename(columns=rename_dict, inplace=True) # Need to reverse symmetry measures df['volt_peak'] = -df['volt_peak'] df['volt_trough'] = -df['volt_trough'] df['time_rdsym'] = 1 - df['time_rdsym'] df['time_ptsym'] = 1 - df['time_ptsym'] return df
import numpy as np import scipy as sp from scipy import signal as spsignal import matplotlib.pyplot as plt from bycycle.filt import amp_by_time, phase_by_time, bandpass_filter from bycycle.sim import sim_noisy_bursty_oscillator signal = np.load('data/sim_bursting_more_noise.npy') Fs = 1000 # Sampling rate f_alpha = (8, 12) N_seconds_filter = .5 # Compute amplitude and phase signal_filt = bandpass_filter(signal, Fs, f_alpha, N_seconds=N_seconds_filter) theta_amp = amp_by_time(signal, Fs, f_alpha, filter_kwargs={'N_seconds': N_seconds_filter}) theta_phase = phase_by_time(signal, Fs, f_alpha, filter_kwargs={'N_seconds': N_seconds_filter}) # Plots signal t = np.arange(0, len(signal) / Fs, 1 / Fs) tlim = (2, 6) tidx = np.logical_and(t >= tlim[0], t < tlim[1]) plt.figure(figsize=(12, 6)) plt.subplot(3, 1, 1) plt.plot(t[tidx], signal[tidx], 'k') plt.xlim(tlim)