Beispiel #1
0
def compute_cyclepoints(sig, fs, f_range, **find_extrema_kwargs):
    """Compute sample indices of cyclepoints.

    Parameters
    ----------
    sig : 1d array
        Time series.
    fs : float
        Sampling rate, in Hz.
    f_range : tuple of (float, float)
        Frequency range, in Hz, to narrowband filter the signal. Used to find zero-crossings.
    find_extrema_kwargs : dict, optional, default: None
        Keyword arguments for the function to find peaks and troughs (:func:`~.find_extrema`)
        that change filter parameters or boundary. By default, the boundary is set to zero.

    Returns
    -------
    df_samples : pandas.DataFrame
        Dataframe containing sample indices of cyclepoints.
        Columns (listed for peak-centered cycles):

        - ``peaks`` : signal indices of oscillatory peaks
        - ``troughs`` :  signal indices of oscillatory troughs
        - ``rises`` : signal indices of oscillatory rising zero-crossings
        - ``decays`` : signal indices of oscillatory decaying zero-crossings
        - ``sample_peak`` : sample 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

    Examples
    --------
    Compute the signal indices of cyclepoints:

    >>> from neurodsp.sim import sim_bursty_oscillation
    >>> fs = 500
    >>> sig = sim_bursty_oscillation(10, fs, freq=10)
    >>> df_samples = compute_cyclepoints(sig, fs, f_range=(8, 12))
    """

    # Ensure arguments are within valid range
    check_param_range(fs, 'fs', (0, np.inf))

    # Find extrema and zero-crossings locations in the signal
    peaks, troughs = find_extrema(sig, fs, f_range, **find_extrema_kwargs)
    rises, decays = find_zerox(sig, peaks, troughs)

    # For each cycle, identify the sample of each extrema and zero-crossing
    samples = {}
    samples['sample_peak'] = peaks[1:]
    samples['sample_last_zerox_decay'] = decays[:-1]
    samples['sample_zerox_decay'] = decays[1:]
    samples['sample_zerox_rise'] = rises
    samples['sample_last_trough'] = troughs[:-1]
    samples['sample_next_trough'] = troughs[1:]

    df_samples = pd.DataFrame.from_dict(samples)

    return df_samples
Beispiel #2
0
def detect_bursts_amp(df_features, burst_fraction_threshold=1, min_n_cycles=3):
    """Detect bursts based on amplitude thresholding.

    Parameters
    ----------
    df_features : pandas.DataFrame
        Waveform features for individual cycles from :func:`~.compute_burst_features`.
    burst_fraction_threshold : int or float, optional, default: 1
        Minimum fraction of a cycle to be identified as a burst.
    min_n_cycles : int, optional, default: 3
        The minimum number of cycles of consecutive cycles required to be considered a burst.

    Returns
    -------
    df_features : pandas.DataFrame
        Dataframe updated, with a additional column to indicate if the cycle is part of a burst.

    Examples
    --------
    Apply thresholding for dual amplitude burst detection:

    >>> from bycycle.features import compute_burst_features, compute_shape_features
    >>> from neurodsp.sim import sim_bursty_oscillation
    >>> fs = 500
    >>> sig = sim_bursty_oscillation(10, fs, freq=10)
    >>> df_shapes = compute_shape_features(sig, fs, f_range=(8, 12))
    >>> df_burst = compute_burst_features(df_shapes, sig, burst_method='amp',
    ...                                   burst_kwargs={'fs': fs, 'f_range': (8, 12)})
    >>> df_burst = detect_bursts_amp(df_burst)
    """

    # Ensure arguments are within valid ranges
    check_param_range(burst_fraction_threshold, 'burst_fraction_threshold',
                      (0, 1))

    # Determine cycles that are defined as bursting throughout the whole cycle
    is_burst = [
        frac >= burst_fraction_threshold
        for frac in df_features['burst_fraction']
    ]

    df_features['is_burst'] = check_min_burst_cycles(is_burst,
                                                     min_n_cycles=min_n_cycles)

    return df_features
Beispiel #3
0
def limit_signal(times, sig, start=None, stop=None):
    """Restrict signal and times to be within time limits.

    Parameters
    ----------
    times : 1d array
        Time definition for the time series.
    sig : 1d array
        Time series.
    start : float
        The lower time limit, in seconds, to restrict the signal.
    stop : float
        The upper time limit, in seconds, to restrict the signal.

    Returns
    -------
    sig : 1d array
        A limited time series.
    times : 1d array
        A limited time definition.

    Examples
    --------
    Restrict a signal and times to the first second:

    >>> from neurodsp.sim import sim_bursty_oscillation
    >>> from neurodsp.utils import create_times
    >>> sig = sim_bursty_oscillation(n_seconds=10, fs=500, freq=10)
    >>> times = create_times(n_seconds=10, fs=500)
    >>> sig, times = limit_signal(times, sig, start=0, stop=1)
    """

    # Ensure arguments are within valid range
    check_param_range(start, 'start', (0, stop))
    check_param_range(stop, 'stop', (start, np.inf))

    if start is not None:
        sig = sig[times >= start]
        times = times[times >= start]

    if stop is not None:
        sig = sig[times < stop]
        times = times[times < stop]

    return sig, times
Beispiel #4
0
def check_min_burst_cycles(is_burst, min_n_cycles=3):
    """Enforce minimum number of consecutive cycles to be considered a burst.

    Parameters
    ----------
    is_burst : 1d array
        Boolean array indicating which cycles are bursting.
    min_n_cycles : int, optional, default: 3
        The minimum number of cycles of consecutive cycles required to be considered a burst.

    Returns
    -------
    is_burst : 1d array
        Updated burst array.

    Examples
    --------
    Remove bursts with less than 3 consecutive cycles:

    >>> is_burst = np.array([False, True, True, False, True, True, True, True, False])
    >>> check_min_burst_cycles(is_burst)
    array([False, False, False, False,  True,  True,  True,  True, False])
    """

    # Ensure argument is within valid range
    check_param_range(min_n_cycles, 'min_n_cycles', (0, np.inf))

    temp_cycle_count = 0

    for idx, bursting in enumerate(is_burst):

        if bursting:
            temp_cycle_count += 1

        else:

            if temp_cycle_count < min_n_cycles:
                for c_rm in range(temp_cycle_count):
                    is_burst[idx - 1 - c_rm] = False

            temp_cycle_count = 0

    return is_burst
Beispiel #5
0
def limit_df(df, fs, start=None, stop=None):
    """Restrict dataframe to be within time limits.

    Parameters
    ----------
    df : pandas.DataFrame
        Dataframe output of :func:`~.compute_features`.
    fs : float
        Sampling rate, in Hz.
    start : float, optional
        The lower time limit, in seconds, to restrict the df.
    stop : float, optional
        The upper time limit, in seconds, to restrict the df.

    Returns
    -------
    df : pandas.DataFrame
        A limited dataframe of cycle features.

    Notes
    -----
    Cycles, or rows in the `df`, are included if any segment of the cycle falls after the
    `stop` time or before the `end` time.

    Examples
    --------
    Limit a samples dataframe to the first second of a simulated signal:

    >>> from neurodsp.sim import sim_bursty_oscillation
    >>> from bycycle.features import compute_features
    >>> fs = 500
    >>> sig = sim_bursty_oscillation(10, fs, freq=10)
    >>> df_features = compute_features(sig, fs, f_range=(8, 12))
    >>> df_features = limit_df(df_features, fs, start=0, stop=1)
    """

    # Ensure arguments are within valid range
    check_param_range(fs, 'fs', (0, np.inf))
    check_param_range(start, 'start', (0, stop))
    check_param_range(stop, 'stop', (start, np.inf))

    center_e, side_e = get_extrema_df(df)

    start = 0 if start is None else start

    df = df[df['sample_next_' + side_e].values >= start*fs]

    if stop is not None:
        df = df[df['sample_last_' + side_e].values < stop*fs]

    # Shift sample indices to start at 0
    df['sample_last_' + side_e] = df['sample_last_' + side_e] - int(fs * start)
    df['sample_next_' + side_e] = df['sample_next_' + side_e] - int(fs * start)
    df['sample_' + center_e] = df['sample_' + center_e] - int(fs * start)
    df['sample_zerox_rise'] = df['sample_zerox_rise'] - int(fs * start)
    df['sample_zerox_decay'] = df['sample_zerox_decay'] - int(fs * start)

    return df
Beispiel #6
0
def find_extrema(sig,
                 fs,
                 f_range,
                 boundary=0,
                 first_extrema='peak',
                 filter_kwargs=None,
                 pass_type='bandpass',
                 pad=True):
    """Identify peaks and troughs in a time series.

    Parameters
    ----------
    sig : 1d array
        Time series.
    fs : float
        Sampling rate, in Hz.
    f_range : tuple of (float, float)
        Frequency range, in Hz, to narrowband filter the signal, used to find zero-crossings.
    boundary : int, optional, default: 0
        Number of samples from edge of the signal to ignore.
    first_extrema: {'peak', 'trough', None}
        If 'peak', then force the output to begin with a peak and end in a trough.
        If 'trough', then force the output to begin with a trough and end in peak.
        If None, force nothing.
    filter_kwargs : dict, optional, default: None
        Keyword arguments to :func:`~neurodsp.filt.filter.filter_signal`,
        such as 'n_cycles' or 'n_seconds' to control filter length.
    pass_type : str, optional, default: 'bandpass'
        Which kind of filter pass_type is consistent with the frequency definition provided.
    pad : bool, optional, default: True
        Whether to pad ``sig`` with zeros to prevent missed cyclepoints at the edges.

    Returns
    -------
    peaks : 1d array
        Indices at which oscillatory peaks occur in the input ``sig``.
    troughs : 1d array
        Indices at which oscillatory troughs occur in the input ``sig``.

    Notes
    -----
    This function assures that there are the same number of peaks and troughs
    if the first extrema is forced to be either peak or trough.

    Examples
    --------
    Find the locations of peaks and burst in a signal:

    >>> from neurodsp.sim import sim_bursty_oscillation
    >>> fs = 500
    >>> sig = sim_bursty_oscillation(10, fs, freq=10)
    >>> peaks, troughs = find_extrema(sig, fs, f_range=(8, 12))
    """

    # Ensure arguments are within valid range
    check_param_range(fs, 'fs', (0, np.inf))

    # Set default filtering parameters
    if filter_kwargs is None:
        filter_kwargs = {}

    # Get the original signal and filter lengths
    sig_len = len(sig)
    filt_len = 0

    # Pad beginning of signal with zeros to prevent missing cyclepoints
    if pad:

        filt_len = compute_filter_length(
            fs,
            pass_type,
            f_range[0],
            f_range[1],
            n_seconds=filter_kwargs.get('n_seconds', None),
            n_cycles=filter_kwargs.get('n_cycles', 3))

        # Pad the signal
        sig = np.pad(sig, int(np.ceil(filt_len / 2)), mode='constant')

    # Narrowband filter signal
    sig_filt = filter_signal(sig,
                             fs,
                             pass_type,
                             f_range,
                             remove_edges=False,
                             **filter_kwargs)

    # Find rising and decaying zero-crossings (narrowband)
    rise_xs = find_flank_zerox(sig_filt, 'rise')
    decay_xs = find_flank_zerox(sig_filt, 'decay')

    # Compute number of peaks and troughs
    if rise_xs[-1] > decay_xs[-1]:
        n_peaks = len(rise_xs) - 1
        n_troughs = len(decay_xs)
    else:
        n_peaks = len(rise_xs)
        n_troughs = len(decay_xs) - 1

    # Calculate peak samples
    peaks = np.zeros(n_peaks, dtype=int)
    for p_idx in range(n_peaks):

        # Calculate the sample range between the most recent zero rise and the next zero decay
        last_rise = rise_xs[p_idx]
        next_decay = decay_xs[decay_xs > last_rise][0]
        # Identify time of peak
        peaks[p_idx] = np.argmax(sig[last_rise:next_decay]) + last_rise

    # Calculate trough samples
    troughs = np.zeros(n_troughs, dtype=int)
    for t_idx in range(n_troughs):

        # Calculate the sample range between the most recent zero decay and the next zero rise
        last_decay = decay_xs[t_idx]
        next_rise = rise_xs[rise_xs > last_decay][0]
        # Identify time of trough
        troughs[t_idx] = np.argmin(sig[last_decay:next_rise]) + last_decay

    # Remove padding
    peaks = peaks - int(np.ceil(filt_len / 2))
    troughs = troughs - int(np.ceil(filt_len / 2))

    # Remove peaks and trough outside the boundary limit
    peaks = peaks[np.logical_and(peaks > boundary, peaks < sig_len - boundary)]
    troughs = troughs[np.logical_and(troughs > boundary,
                                     troughs < sig_len - boundary)]

    # Force the first extrema to be as desired & assure equal # of peaks and troughs
    if first_extrema == 'peak':
        troughs = troughs[1:] if peaks[0] > troughs[0] else troughs
        peaks = peaks[:-1] if peaks[-1] > troughs[-1] else peaks
    elif first_extrema == 'trough':
        peaks = peaks[1:] if troughs[0] > peaks[0] else peaks
        troughs = troughs[:-1] if troughs[-1] > peaks[-1] else troughs
    elif first_extrema is None:
        pass
    else:
        raise ValueError('Parameter "first_extrema" is invalid')

    return peaks, troughs
Beispiel #7
0
def compute_burst_fraction(df_samples,
                           sig,
                           fs,
                           f_range,
                           amp_threshes=(1, 2),
                           min_n_cycles=3,
                           min_burst_duration=None,
                           filter_kwargs=None):
    """Compute the proportion of each cycle that is bursting using a dual threshold algorithm.

    Parameters
    ----------
    df_samples : pandas.DataFrame
        Indices of cyclepoints returned from :func:`~.compute_cyclepoints`.
    sig : 1d array
        Time series.
    fs : float
        Sampling rate, Hz.
    f_range : tuple of (float, float)
        Frequency range (Hz) for oscillaton of interest.
    amp_threshes : tuple (low, high), optional, default: (1, 2)
        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).
    min_n_cycles : int, optional, default: 3
        Minimum number of consecutive cycles to be identified as an oscillation.
    min_burst_duration : float, optional, default: None
        Minimum length of a burst, in seconds.
    filter_kwargs : dict, optional, default: None
        Keyword arguments to :func:`~neurodsp.filt.filter.filter_signal`.

    Returns
    -------
    burst_fraction : 1d array
        The proportion of each cycle that is bursting.

    Notes
    -----
    If a cycle contains three samples and the corresponding section of `is_burst` is
    np.array([True, True, False]), the burst fraction is 0.66 for that cycle.

    Examples
    --------
    Compute proportions of cycles that are bursting using dual amplitude thresholding:

    >>> from bycycle.features import compute_cyclepoints
    >>> from neurodsp.sim import sim_bursty_oscillation
    >>> fs = 500
    >>> sig = sim_bursty_oscillation(10, fs, freq=10)
    >>> df_samples = compute_cyclepoints(sig, fs, f_range=(8, 12))
    >>> burst_fraction = compute_burst_fraction(df_samples, sig, fs, f_range=(8, 12))
    """

    # Ensure arguments are within valid ranges
    check_param_range(fs, 'fs', (0, np.inf))
    check_param_range(amp_threshes[0], 'lower amp_threshes',
                      (0, amp_threshes[1]))
    check_param_range(amp_threshes[1], 'upper amp_threshes',
                      (amp_threshes[0], np.inf))

    filter_kwargs = {} if filter_kwargs is None else filter_kwargs

    # Detect bursts using the dual amplitude threshold approach
    if min_burst_duration is not None:
        min_n_cycles = None

    is_burst = detect_bursts_dual_threshold(
        sig,
        fs,
        amp_threshes,
        f_range,
        min_n_cycles=min_n_cycles,
        min_burst_duration=min_burst_duration,
        **filter_kwargs)

    # Convert the boolean array to binary
    is_burst = is_burst.astype(int)

    # Determine cycle sides
    side_e = 'trough' if 'sample_peak' in df_samples.columns else 'peak'

    # Compute fraction of each cycle that's bursting
    burst_fraction = []
    for _, row in df_samples.iterrows():
        fraction_bursting = np.mean(
            is_burst[int(row['sample_last_' +
                             side_e]):int(row['sample_next_' + side_e] + 1)])
        burst_fraction.append(fraction_bursting)

    return burst_fraction
Beispiel #8
0
def plot_cyclepoints_df(df_samples,
                        sig,
                        fs,
                        plot_sig=True,
                        plot_extrema=True,
                        plot_zerox=True,
                        xlim=None,
                        ax=None,
                        **kwargs):
    """Plot extrema and/or zero-crossings from a DataFrame.

    Parameters
    ----------
    df_samples : pandas.DataFrame
        Dataframe output of :func:`~.compute_cyclepoints`.
    sig : 1d array
        Time series to plot.
    fs : float
        Sampling rate, in Hz.
    plot_sig : bool, optional, default: True
        Whether to also plot the raw signal.
    plot_extrema :  bool, optional, default: True
        Whether to plots the peaks and troughs.
    plot_zerox :  bool, optional, default: True
        Whether to plots the zero-crossings.
    xlim : tuple of (float, float), optional
        Start and stop times.
    ax : matplotlib.Axes, optional
        Figure axes upon which to plot.
    **kwargs
        Keyword arguments to pass into `plot_time_series`.

    Notes
    -----
    Default keyword arguments include:

    - ``figsize``: tuple of (float, float), default: (15, 3)
    - ``xlabel``: str, default: 'Time (s)'
    - ``ylabel``: str, default: 'Voltage (uV)

    Examples
    --------
    Plot cyclepoints using a dataframe from :func:`~.compute_cyclepoints`:

    >>> from bycycle.features import compute_cyclepoints
    >>> from neurodsp.sim import sim_bursty_oscillation
    >>> fs = 500
    >>> sig = sim_bursty_oscillation(10, fs, freq=10)
    >>> df_samples = compute_cyclepoints(sig, fs, f_range=(8, 12))
    >>> plot_cyclepoints_df(df_samples, sig, fs)
    """

    # Ensure arguments are within valid range
    check_param_range(fs, 'fs', (0, np.inf))

    # Determine extrema/zero-crossings from dataframe
    center_e, side_e = get_extrema_df(df_samples)

    peaks, troughs, rises, decays = [None] * 4

    if plot_extrema:

        peaks = df_samples['sample_' + center_e].values
        troughs = np.append(df_samples['sample_last_' + side_e].values,
                            df_samples['sample_next_' + side_e].values)
        troughs = np.unique(troughs)

    if plot_zerox:

        rises = df_samples['sample_zerox_rise'].values
        decays = df_samples['sample_zerox_decay'].values

    plot_cyclepoints_array(sig,
                           fs,
                           peaks=peaks,
                           troughs=troughs,
                           rises=rises,
                           decays=decays,
                           plot_sig=plot_sig,
                           xlim=xlim,
                           ax=ax,
                           **kwargs)
Beispiel #9
0
def plot_cyclepoints_array(sig,
                           fs,
                           peaks=None,
                           troughs=None,
                           rises=None,
                           decays=None,
                           plot_sig=True,
                           xlim=None,
                           ax=None,
                           **kwargs):
    """Plot extrema and/or zero-crossings from arrays.

    Parameters
    ----------
    sig : 1d array
        Time series to plot.
    fs : float
        Sampling rate, in Hz.
    peaks : 1d array, optional
        Peak signal indices from :func:`.find_extrema`.
    troughs : 1d array, optional
        Trough signal indices from :func:`.find_extrema`.
    rises : 1d array, optional
        Zero-crossing rise indices from :func:`~.find_zerox`.
    decays : 1d array, optional
        Zero-crossing decay indices from :func:`~.find_zerox`.
    plot_sig : bool, optional, default: True
        Whether to also plot the raw signal.
    xlim : tuple of (float, float), optional
        Start and stop times.
    ax : matplotlib.Axes, optional, default: None
        Figure axes upon which to plot.
    **kwargs
        Keyword arguments to pass into `plot_time_series`.

    Notes
    -----
    Default keyword arguments include:

    - ``figsize``: tuple of (float, float), default: (15, 3)
    - ``xlabel``: str, default: 'Time (s)'
    - ``ylabel``: str, default: 'Voltage (uV)
    - ``colors``: list, default: ['k', 'b', 'r', 'g', 'm']

    Examples
    --------
    Plot cyclepoints using arrays from :func:`.find_extrema` and  :func:`~.find_zerox`:

    >>> from bycycle.cyclepoints import find_extrema, find_zerox
    >>> from neurodsp.sim import sim_bursty_oscillation
    >>> fs = 500
    >>> sig = sim_bursty_oscillation(10, fs, freq=10)
    >>> peaks, troughs = find_extrema(sig, fs, f_range=(8, 12), boundary=0)
    >>> rises, decays = find_zerox(sig, peaks, troughs)
    >>> plot_cyclepoints_array(sig, fs, peaks=peaks, troughs=troughs, rises=rises, decays=decays)
    """

    # Ensure arguments are within valid range
    check_param_range(fs, 'fs', (0, np.inf))

    # Set times and limits
    times = np.arange(0, len(sig) / fs, 1 / fs)

    # Restrict sig and times to xlim
    if xlim is not None:
        sig, times = limit_signal(times, sig, start=xlim[0], stop=xlim[1])

    # Set default kwargs
    figsize = kwargs.pop('figsize', (15, 3))
    xlabel = kwargs.pop('xlabel', 'Time (s)')
    ylabel = kwargs.pop('ylabel', 'Voltage (uV)')
    default_colors = ['b', 'r', 'g', 'm']

    # Extend plotting based on given arguments
    x_values = []
    y_values = []
    colors = ['k']

    for idx, points in enumerate([peaks, troughs, rises, decays]):

        if points is not None:

            # Limit times and shift indices of cyclepoints (cps)
            cps = points[(points >= times[0] * fs) & (points < times[-1] * fs)]
            cps = cps - int(times[0] * fs)

            y_values.append(sig[cps])
            x_values.append(times[cps])
            colors.append(default_colors[idx])

    # Allow custom colors to overwrite default
    colors = kwargs.pop('colors', colors)

    if ax is None:
        fig, ax = plt.subplots(figsize=figsize)

    if plot_sig:
        plot_time_series(times, sig, colors=colors[0], ax=ax)
        colors = colors[1:]

    plot_time_series(x_values,
                     y_values,
                     ax=ax,
                     xlabel=xlabel,
                     ylabel=ylabel,
                     colors=colors,
                     marker='o',
                     ls='',
                     **kwargs)
Beispiel #10
0
def compute_features(sig,
                     fs,
                     f_range,
                     center_extrema='peak',
                     burst_method='cycles',
                     burst_kwargs=None,
                     threshold_kwargs=None,
                     find_extrema_kwargs=None,
                     return_samples=True):
    """Compute shape and burst features for each cycle.

    Parameters
    ----------
    sig : 1d array
        Time series.
    fs : float
        Sampling rate, in Hz.
    f_range : tuple of (float, float)
        Frequency range for narrowband signal of interest (Hz).
    center_extrema : {'peak', 'trough'}
        The center extrema in the cycle.

        - 'peak' : cycles are defined trough-to-trough
        - 'trough' : cycles are defined peak-to-peak

    burst_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_kwargs : dict, optional, default: None
        Additional keyword arguments defined in :func:`~.compute_burst_fraction` for dual
        amplitude threshold burst detection (i.e. when burst_method='amp').
    threshold_kwargs : dict, optional, default: None
        Feature thresholds for cycles to be considered bursts, matching keyword arguments for:

        - :func:`~.detect_bursts_cycles` for consistency burst detection
          (i.e. when burst_method='cycles')
        - :func:`~.detect_bursts_amp` for  amplitude threshold burst detection
          (i.e. when burst_method='amp').

    find_extrema_kwargs : dict, optional, default: None
        Keyword arguments for function to find peaks an troughs (:func:`~.find_extrema`)
        to change filter parameters or boundary. By default, the filter length is set to three
        cycles of the low cutoff frequency (``f_range[0]``).
    return_samples : bool, optional, default: True
        Returns samples indices of cyclepoints used for determining features if True.

    Returns
    -------
    df_features : pandas.DataFrame
        A dataframe containing shape and burst features for each cycle. Columns:

        - ``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

        When consistency burst detection is used (i.e. burst_method='cycles'):

        - ``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 monotonic voltage changes in rise and decay phases
          (positive going in rise and negative going in decay)

        When dual threshold burst detection is used (i.e. burst_method='amp'):

        - ``burst_fraction`` : fraction of a cycle that is bursting

        When cyclepoints are returned (i.e. default, return_samples=True)

        - ``sample_peak`` : sample 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

    Examples
    --------
    Compute shape and burst features:

    >>> from neurodsp.sim import sim_bursty_oscillation
    >>> fs = 500
    >>> sig = sim_bursty_oscillation(10, fs, freq=10)
    >>> df_features = compute_features(sig, fs, f_range=(8, 12))
    """

    # Ensure arguments are within valid range
    check_param_range(fs, 'fs', (0, np.inf))

    # Compute shape features for each cycle
    df_shape_features = compute_shape_features(
        sig,
        fs,
        f_range,
        center_extrema=center_extrema,
        find_extrema_kwargs=find_extrema_kwargs)

    # Ensure kwargs are a dictionaries
    if burst_method == 'amp' and not isinstance(burst_kwargs, dict):
        burst_kwargs = {}

    if not isinstance(threshold_kwargs, dict):
        threshold_kwargs = {}
        warnings.warn("""
            No burst detection thresholds are provided. This is not recommended. Please
            inspect your data and choose appropriate parameters for 'threshold_kwargs'.
            Default burst detection parameters are likely not well suited for your
            desired application.
            """)

    # Ensure required kwargs are set for amplitude burst detection
    if burst_method == 'amp':
        burst_kwargs['fs'] = fs
        burst_kwargs['f_range'] = f_range

    # Compute burst features for each cycle
    df_burst_features = compute_burst_features(df_shape_features,
                                               sig,
                                               burst_method=burst_method,
                                               burst_kwargs=burst_kwargs)

    # Concatenate shape and burst features
    df_features = pd.concat((df_burst_features, df_shape_features), axis=1)

    # Define whether or not each cycle is part of a burst
    if burst_method == 'cycles':
        df_features = detect_bursts_cycles(df_features, **threshold_kwargs)
    elif burst_method == 'amp':
        df_features = detect_bursts_amp(df_features, **threshold_kwargs)
    else:
        raise ValueError('Invalid argument for "burst_method".'
                         'Either "cycles" or "amp" must be specified."')

    df_features = drop_samples_df(
        df_features) if return_samples is False else df_features

    return df_features
Beispiel #11
0
def plot_burst_detect_summary(df_features,
                              sig,
                              fs,
                              threshold_kwargs,
                              xlim=None,
                              figsize=(15, 3),
                              plot_only_result=False,
                              interp=True):
    """Plot the cycle-by-cycle burst detection parameters and burst detection summary.

    Parameters
    ----------
    df_features : pandas.DataFrame
        Dataframe output of :func:`~.compute_features`. The df must contain sample indices (i.e.
        when ``return_samples = True``).
    sig : 1d array
        Time series to plot.
    fs : float
        Sampling rate, in Hz.
    threshold_kwargs : dict
        Burst parameter keys and threshold value pairs, as defined in the 'threshold_kwargs'
        argument of :func:`.compute_features`.
    xlim : tuple of (float, float), optional, default: None
        Start and stop times for plot.
    figsize : tuple of (float, float), optional, default: (15, 3)
        Size of each plot.
    plot_only_result : bool, optional, default: False
        Plot only the signal and bursts, excluding burst parameter plots.
    interp : bool, optional, default: True
        If True, interpolates between given values. Otherwise, plots in a step-wise fashion.

    Notes
    -----

    - If plot_only_result = True: return a plot of the burst detection in which periods with bursts
      are denoted in red.

    - If plot_only_result = False: return a list of the fig handle followed by the 5 axes.

    - In the top plot, the raw signal is plotted in black, and the red line indicates periods
      defined as oscillatory bursts. The highlighted regions indicate when each burst requirement
      was violated, color-coded consistently with the plots below.

      - blue: amp_fraction_threshold
      - red: amp_consistency_threshold
      - yellow: period_consistency_threshold
      - green: monotonicity_threshold

    Examples
    --------
    Plot the burst detection summary of a bursting signal:

    >>> from bycycle.features import compute_features
    >>> from neurodsp.sim import sim_bursty_oscillation
    >>> fs = 500
    >>> sig = sim_bursty_oscillation(10, fs, freq=10)
    >>> threshold_kwargs = {'amp_fraction_threshold': 0., 'amp_consistency_threshold': .5,
    ...                     'period_consistency_threshold': .5, 'monotonicity_threshold': .8}
    >>> df_features = compute_features(sig, fs, f_range=(8, 12), threshold_kwargs=threshold_kwargs)
    >>> plot_burst_detect_summary(df_features, sig, fs, threshold_kwargs)
    """

    # Ensure arguments are within valid range
    check_param_range(fs, 'fs', (0, np.inf))

    # Normalize signal
    sig = zscore(sig)

    # Determine time array and limits
    times = np.arange(0, len(sig) / fs, 1 / fs)
    xlim = (times[0], times[-1]) if xlim is None else xlim

    # Determine if peak of troughs are the sides of an oscillation
    _, side_e = get_extrema_df(df_features)

    # Remove this kwarg since it isn't stored cycle by cycle in the df (nothing to plot)
    thresholds = threshold_kwargs.copy()
    if 'min_n_cycles' in thresholds.keys():
        del thresholds['min_n_cycles']

    n_kwargs = len(thresholds.keys())

    # Create figure and subplots
    if plot_only_result:
        fig, axes = plt.subplots(figsize=figsize, nrows=1)
        axes = [axes]
    else:
        fig, axes = plt.subplots(figsize=(figsize[0],
                                          figsize[1] * (n_kwargs + 1)),
                                 nrows=n_kwargs + 1,
                                 sharex=True)

    # Determine which samples are defined as bursting
    is_osc = np.zeros(len(sig), dtype=bool)
    df_osc = df_features.loc[df_features['is_burst']]

    for _, cyc in df_osc.iterrows():

        samp_start_burst = int(cyc['sample_last_' + side_e])
        samp_end_burst = int(cyc['sample_next_' + side_e] + 1)
        is_osc[samp_start_burst:samp_end_burst] = True

    # Plot bursts with extrema points
    xlabel = 'Time (s)' if len(axes) == 1 else ''

    plot_bursts(times,
                sig,
                is_osc,
                ax=axes[0],
                xlim=xlim,
                lw=2,
                labels=['Signal', 'Bursts'],
                xlabel='',
                ylabel='')

    plot_cyclepoints_df(df_features,
                        sig,
                        fs,
                        ax=axes[0],
                        xlim=xlim,
                        plot_zerox=False,
                        plot_sig=False,
                        xlabel=xlabel,
                        ylabel='Voltage\n(normalized)',
                        colors=['m', 'c'])

    # Plot each burst param
    colors = cycle(
        ['blue', 'red', 'yellow', 'green', 'cyan', 'magenta', 'orange'])

    for idx, osc_key in enumerate(thresholds.keys()):

        column = osc_key.replace('_threshold', '')

        color = next(colors)

        # Highlight where a burst param falls below threshold
        for _, cyc in df_features.iterrows():

            if cyc[column] < thresholds[osc_key]:
                axes[0].axvspan(times[int(cyc['sample_last_' + side_e])],
                                times[int(cyc['sample_next_' + side_e])],
                                alpha=0.5,
                                color=color,
                                lw=0)

        # Plot each burst param on separate axes
        if not plot_only_result:

            ylabel = column.replace('_', ' ').capitalize()
            xlabel = 'Time (s)' if idx == n_kwargs - 1 else ''

            plot_burst_detect_param(df_features,
                                    sig,
                                    fs,
                                    column,
                                    thresholds[osc_key],
                                    figsize=figsize,
                                    ax=axes[idx + 1],
                                    xlim=xlim,
                                    xlabel=xlabel,
                                    ylabel=ylabel,
                                    color=color,
                                    interp=interp)
Beispiel #12
0
def plot_burst_detect_param(df_features,
                            sig,
                            fs,
                            burst_param,
                            thresh,
                            xlim=None,
                            interp=True,
                            ax=None,
                            **kwargs):
    """Plot a burst detection parameter and threshold.

    Parameters
    ----------
    df_features : pandas.DataFrame
        Dataframe output of :func:`~.compute_features`.
    sig : 1d array
        Time series to plot.
    fs : float
        Sampling rate, in Hz.
    burst_param : str
        Column name of the parameter of interest in ``df``.
    thresh : float
        The burst parameter threshold. Parameter values greater
        than ``thresh`` are considered bursts.
    xlim : tuple of (float, float), optional, default: None
        Start and stop times for plot.
    interp : bool, optional, default: True
        Interpolates points if true.
    ax : matplotlib.Axes, optional
        Figure axes upon which to plot.
    **kwargs
        Keyword arguments to pass into `plot_time_series`.

    Notes
    -----
    Default keyword arguments include:

    - ``figsize``: tuple of (float, float), default: (15, 3)
    - ``xlabel``: str, default: 'Time (s)'
    - ``ylabel``: str, default: 'Voltage (uV)
    - ``color``: str, default: 'r'.

      - Note: ``color`` here is the fill color, rather than line color.

    Examples
    --------
    Plot the monotonicity of a bursting signal:

    >>> from bycycle.features import compute_features
    >>> from neurodsp.sim import sim_bursty_oscillation
    >>> fs = 500
    >>> sig = sim_bursty_oscillation(10, fs, freq=10)
    >>> threshold_kwargs = {'amp_fraction_threshold': 0., 'amp_consistency_threshold': .5,
    ...                     'period_consistency_threshold': .5, 'monotonicity_threshold': .8}
    >>> df_features = compute_features(sig, fs, f_range=(8, 12),
    ...                                            threshold_kwargs=threshold_kwargs)
    >>> plot_burst_detect_param(df_features, sig, fs, 'monotonicity', .8)
    """

    # Ensure arguments are within valid range
    check_param_range(fs, 'fs', (0, np.inf))

    # Set default kwargs
    figsize = kwargs.pop('figsize', (15, 3))
    xlabel = kwargs.pop('xlabel', 'Time (s)')
    ylabel = kwargs.pop('ylabel', burst_param)
    color = kwargs.pop('color', 'r')

    # Determine time array and limits
    times = np.arange(0, len(sig) / fs, 1 / fs)
    xlim = (times[0], times[-1]) if xlim is None else xlim

    if ax is None:
        fig, ax = plt.subplots(figsize=figsize)

    # Determine extrema strings
    center_e, side_e = get_extrema_df(df_features)

    # Limit dataframe, sig and times
    df = limit_df(df_features, fs, start=xlim[0], stop=xlim[1])

    sig, times = limit_signal(times, sig, start=xlim[0], stop=xlim[1])

    # Remove start / end cycles that tlims falls between
    df = df[(df['sample_last_' + side_e] >= 0) & \
            (df['sample_next_' + side_e] < xlim[1]*fs)]

    # Plot burst param
    if interp:

        plot_time_series([times[df['sample_' + center_e]], xlim],
                         [df[burst_param], [thresh] * 2],
                         ax=ax,
                         colors=['k', 'k'],
                         ls=['-', '--'],
                         marker=["o", None],
                         xlabel=xlabel,
                         ylabel="{0:s}\nthreshold={1:.2f}".format(
                             ylabel, thresh),
                         **kwargs)

    else:

        # Create steps, from side to side of each cycle, and set the y-value
        #   to the burst parameter value for that cycle
        side_times = np.array([])
        side_param = np.array([])

        for _, cyc in df.iterrows():

            # Get the times for the last and next side of a cycle
            side_times = np.append(side_times, [
                times[int(cyc['sample_last_' + side_e])], times[int(
                    cyc['sample_next_' + side_e])]
            ])

            # Set the y-value, from side to side, to the burst param for each cycle
            side_param = np.append(side_param, [cyc[burst_param]] * 2)

        plot_time_series([side_times, xlim], [side_param, [thresh] * 2],
                         ax=ax,
                         colors=['k', 'k'],
                         ls=['-', '--'],
                         marker=["o", None],
                         xlim=xlim,
                         xlabel=xlabel,
                         ylabel="{0:s}\nthreshold={1:.2f}".format(
                             ylabel, thresh),
                         **kwargs)

    # Highlight where param falls below threshold
    for _, cyc in df.iterrows():

        if cyc[burst_param] <= thresh:

            ax.axvspan(times[int(cyc['sample_last_' + side_e])],
                       times[int(cyc['sample_next_' + side_e])],
                       alpha=0.5,
                       color=color,
                       lw=0)
Beispiel #13
0
def detect_bursts_cycles(df_features, amp_fraction_threshold=0., amp_consistency_threshold=.5,
                         period_consistency_threshold=.5, monotonicity_threshold=.8,
                         min_n_cycles=3):
    """Detects bursts based on consistency between cycles.

    Parameters
    ----------
    df_features : pandas.DataFrame
        Waveform features for individual cycles from :func:`~.compute_burst_features`.
    amp_fraction_threshold : float, optional, default: 0.
        The minimum normalized amplitude a cycle must have in order to be considered in an
        oscillation. Must be between 0 and 1.

        - 0 = the minimum amplitude across all cycles
        - .5 = the median amplitude across all cycles
        - 1 = the maximum amplitude across all cycles

    amp_consistency_threshold : float, optional, default: 0.5
        The minimum normalized difference in rise and decay magnitude to be considered as in an
        oscillatory mode. Must be between 0 and 1.

        - 1 = the same amplitude for the rise and decay
        - .5 = the rise (or decay) is half the amplitude of the decay (rise)

    period_consistency_threshold : float, optional, default: 0.5
        The minimum normalized difference in period between two adjacent cycles to be considered
        as in an oscillatory mode. Must be between 0 and 1.

        - 1 = the same period for both cycles
        - .5 = one cycle is half the duration of another cycle

    monotonicity_threshold : float, optional, default: 0.8
        The minimum fraction of time segments between samples that must be going in the same
        direction. Must be between 0 and 1.

        - 1 = rise and decay are perfectly monotonic
        - .5 = both rise and decay are rising half of the time and decay half the time
        - 0 = rise period is all decaying and decay period is all rising

    min_n_cycles : int, optional, default: 3
        The minimum number of cycles of consecutive cycles required to be considered a burst.

    Returns
    -------
    df_features : pandas.DataFrame
        Same df as input, with an additional column (`is_burst`) to indicate if the cycle is part
        of an oscillatory burst, with additional columns indicating the burst detection parameters.

    Notes
    -----
    * The first and last period cannot be considered oscillating if the consistency measures are
      used.

    Examples
    --------
    Apply thresholding for consistency burst detection:

    >>> from bycycle.features import compute_burst_features, compute_shape_features
    >>> from neurodsp.sim import sim_bursty_oscillation
    >>> fs = 500
    >>> sig = sim_bursty_oscillation(10, fs, freq=10)
    >>> df_shapes = compute_shape_features(sig, fs, f_range=(8, 12))
    >>> df_burst = compute_burst_features(df_shapes, sig)
    >>> df_burst = detect_bursts_cycles(df_burst)
    """

    # Ensure arguments are within valid ranges
    check_param_range(amp_fraction_threshold, 'amp_fraction_threshold', (0, 1))
    check_param_range(amp_consistency_threshold, 'amp_consistency_threshold', (0, 1))
    check_param_range(period_consistency_threshold, 'period_consistency_threshold', (0, 1))
    check_param_range(monotonicity_threshold, 'monotonicity_threshold', (0, 1))

    # Compute if each period is part of an oscillation
    amp_fraction = df_features['amp_fraction'] > amp_fraction_threshold
    amp_consistency = df_features['amp_consistency'] > amp_consistency_threshold
    period_consistency = df_features['period_consistency'] > period_consistency_threshold
    monotonicity = df_features['monotonicity'] > monotonicity_threshold

    # Set the burst status for each cycle as the answer across all criteria
    is_burst = amp_fraction & amp_consistency & period_consistency & monotonicity

    # Set the first and last cycles to not be part of a burst
    is_burst[0] = False
    is_burst[-1] = False

    df_features['is_burst'] = check_min_burst_cycles(is_burst, min_n_cycles=min_n_cycles)

    return df_features