示例#1
0
def test_detect_bursts_df_amp():
    """Test amplitude-threshold burst detection."""

    # Load signal
    signal = np.load(DATA_PATH + 'sim_bursting.npy')
    Fs = 1000
    f_range = (6, 14)
    signal = filt.lowpass_filter(signal, Fs, 30, N_seconds=.3,
                                 remove_edge_artifacts=False)

    # Compute cycle-by-cycle df without burst detection column
    df = features.compute_features(signal, Fs, f_range,
                                   burst_detection_method='amp',
                                   burst_detection_kwargs={'amp_threshes': (1, 2),
                                                           'filter_kwargs': {'N_seconds': .5}})
    df.drop('is_burst', axis=1, inplace=True)

    # Apply consistency burst detection
    df_burst_amp = burst.detect_bursts_df_amp(df, signal, Fs, f_range,
                                              amp_threshes=(.5, 1),
                                              N_cycles_min=4, filter_kwargs={'N_seconds': .5})

    # Make sure that burst detection is only boolean
    assert df_burst_amp.dtypes['is_burst'] == 'bool'
    assert df_burst_amp['is_burst'].mean() > 0
    assert df_burst_amp['is_burst'].mean() < 1
    assert np.min([sum(1 for _ in group) for key, group \
        in itertools.groupby(df_burst_amp['is_burst']) if key]) >= 4
示例#2
0
def compute_features(sig,
                     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
    ----------
    sig : 1d array
        Voltage time series.
    fs : float
        Sampling rate, in 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 : {'cycles', 'amp'}
        Method for detecting bursts.

        - 'cycles': detect bursts based on the consistency of consecutive periods & amplitudes
        - 'amp': detect bursts using an amplitude threshold

    burst_detection_kwargs : dict, optional
        Keyword arguments for function to find label cycles as in or not in an oscillation.
    find_extrema_kwargs : dict, optional
        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, optional, default: False
        Corresponding kwarg for :func:`~neurodsp.timefrequency.hilbert.amp_by_time`.
        If true, this zero-pads 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 features and identifiers for each cycle. Each row is one cycle.
        Columns (listed for peak-centered cycles):

        - ``sample_peak`` : sample of 'sig' at which the peak occurs
        - ``sample_zerox_decay`` : sample of the decaying zero-crossing
        - ``sample_zerox_rise`` : sample of the rising zero-crossing
        - ``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 zero-crosses
        - ``time_trough`` : duration of previous trough estimated by zero-crossings
        - ``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 not recommended.
            Check your data and choose appropriate parameters for "burst_detection_kwargs".
            Default burst detection parameters are likely not well suited for the data.
            ''')
    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 assumes 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':
        sig = -sig
    else:
        raise ValueError(
            'Parameter "center_extrema" must be either "P" or "T"')

    # Find peak and trough locations in the signal
    ps, ts = find_extrema(sig, fs, f_range, **find_extrema_kwargs)

    # Find zero-crossings
    zerox_rise, zerox_decay = find_zerox(sig, ps, ts)

    # For each cycle, identify the sample of each extrema and zero-crossing
    shape_features = {}
    shape_features['sample_peak'] = ps[1:]
    shape_features['sample_zerox_decay'] = zerox_decay[1:]
    shape_features['sample_zerox_rise'] = zerox_rise
    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'] = zerox_rise - zerox_decay[:-1]

    # Determine extrema voltage
    shape_features['volt_peak'] = sig[ps[1:]]
    shape_features['volt_trough'] = sig[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'] = sig[ps[1:]] - sig[ts[1:]]
    shape_features['volt_rise'] = sig[ps[1:]] - sig[ts[:-1]]
    shape_features['volt_amp'] = (shape_features['volt_decay'] +
                                  shape_features['volt_rise']) / 2

    # Compute 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(sig,
                      fs,
                      f_range,
                      hilbert_increase_n=hilbert_increase_n,
                      n_cycles=3)
    shape_features['band_amp'] = [
        np.mean(amp[ts[sig_idx]:ts[sig_idx + 1]])
        for sig_idx 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, sig, **burst_detection_kwargs)
    elif burst_detection_method == 'amp':
        df = detect_bursts_df_amp(df, sig, 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
示例#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
        'cycles': detect bursts based on the consistency of consecutive periods and 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 and
        troughs (cyclepoints.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 filt.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

    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