Example #1
0
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)))
Example #2
0
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
Example #3
0
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)