Example #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(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
Example #2
0
def test_extrema_interpolated_phase():
    """Test waveform phase estimate."""

    # Load signal
    signal = np.load(DATA_PATH + 'sim_stationary.npy')
    Fs = 1000
    f_range = (6, 14)

    # Find peaks and troughs
    Ps, Ts = cyclepoints.find_extrema(signal,
                                      Fs,
                                      f_range,
                                      boundary=1,
                                      first_extrema='peak')

    # Find zerocrossings
    zeroxR, zeroxD = cyclepoints.find_zerox(signal, Ps, Ts)

    # Compute phase
    pha = cyclepoints.extrema_interpolated_phase(signal,
                                                 Ps,
                                                 Ts,
                                                 zeroxR=zeroxR,
                                                 zeroxD=zeroxD)
    assert len(pha) == len(signal)
    assert np.all(np.isclose(pha[Ps], 0))
    assert np.all(np.isclose(pha[Ts], -np.pi))
    assert np.all(np.isclose(pha[zeroxR], -np.pi / 2))
    assert np.all(np.isclose(pha[zeroxD], np.pi / 2))
Example #3
0
def test_extrema_interpolated_phase(sim_stationary):
    """Test waveform phase estimate."""

    sig = sim_stationary

    fs = 1000
    f_range = (6, 14)

    # Find peaks and troughs
    peaks, troughs = find_extrema(sig,
                                  fs,
                                  f_range,
                                  boundary=1,
                                  first_extrema='peak')

    # Find zerocrossings
    rises, decays = find_zerox(sig, peaks, troughs)

    # Compute phase
    pha = extrema_interpolated_phase(sig,
                                     peaks,
                                     troughs,
                                     rises=rises,
                                     decays=decays)

    assert len(pha) == len(sig)
    assert np.all(np.isclose(pha[peaks], 0))
    assert np.all(np.isclose(pha[troughs], -np.pi))
    assert np.all(np.isclose(pha[rises], -np.pi / 2))
    assert np.all(np.isclose(pha[decays], np.pi / 2))
Example #4
0
def test_plot_cyclepoints_array(sim_args):

    peaks, troughs = find_extrema(sim_args['sig'], sim_args['fs'], (6, 14))
    rises, decays = find_zerox(sim_args['sig'], peaks, troughs)

    plot_cyclepoints_array(sim_args['sig'], sim_args['fs'], peaks=peaks, troughs=troughs,
                           rises=rises, decays=decays, save_fig=True,
                           file_name='test_plot_cyclepoints_array', file_path=TEST_PLOTS_PATH)
Example #5
0
def test_plot_cyclepoints_array(sim_args):

    ps, ts = find_extrema(sim_args['sig'], sim_args['fs'], (6, 14))
    zerox_rise, zerox_decay = find_zerox(sim_args['sig'], ps, ts)

    plot_cyclepoints_array(sim_args['sig'],
                           sim_args['fs'],
                           ps=ps,
                           ts=ts,
                           zerox_rise=zerox_rise,
                           zerox_decay=zerox_decay,
                           save_fig=True,
                           file_name='test_plot_cyclepoints_array',
                           file_path=TEST_PLOTS_PATH)
Example #6
0
def test_compute_cyclepoints(sim_args):

    sig = sim_args['sig']
    fs = sim_args['fs']
    f_range = sim_args['f_range']

    peaks, troughs = find_extrema(sig, fs, f_range)
    rises, decays = find_zerox(sig, peaks, troughs)

    df_samples = compute_cyclepoints(sig, fs, f_range)

    assert (df_samples['sample_peak'] == peaks[1:]).all()
    assert (df_samples['sample_zerox_decay'] == decays[1:]).all()
    assert (df_samples['sample_zerox_rise'] == rises).all()
    assert (df_samples['sample_last_trough'] == troughs[:-1]).all()
    assert (df_samples['sample_next_trough'] == troughs[1:]).all()
Example #7
0
def test_find_zerox():
    """Test ability to find peaks and troughs."""

    # Load signal
    signal = np.load(DATA_PATH + 'sim_stationary.npy')
    Fs = 1000
    f_range = (6, 14)

    # Find peaks and troughs
    Ps, Ts = cyclepoints.find_extrema(signal,
                                      Fs,
                                      f_range,
                                      boundary=1,
                                      first_extrema='peak')

    # Find zerocrossings
    zeroxR, zeroxD = cyclepoints.find_zerox(signal, Ps, Ts)
    assert len(Ps) == (len(zeroxR) + 1)
    assert len(Ts) == len(zeroxD)
    assert Ps[0] < zeroxD[0]
    assert zeroxD[0] < Ts[0]
    assert Ts[0] < zeroxR[0]
    assert zeroxR[0] < Ps[1]
Example #8
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
Example #9
0
####################################################################################################
#
# 2. Localize rise and decay midpoints
# ------------------------------------
#
# In addition to localizing the peaks and troughs of a cycle, we also want to get more information
# about the rise and decay periods. For instance, these flanks may have deflections if the peaks or
# troughs are particularly sharp. In order to gauge a dimension of this, we localize midpoints for
# each of the rise and decay segments. These midpoints are defined as the times at which the voltage
# crosses halfway between the adjacent peak and trough voltages. If this threshold is crossed
# multiple times, then the median time is chosen as the flank midpoint. This is not perfect;
# however, this is rare, and most of these cycles should be removed by burst detection.

from bycycle.cyclepoints import find_zerox
zeroxR, zeroxD = find_zerox(signal_low, Ps, Ts)

####################################################################################################

tlim = (13, 14)
tidx = np.logical_and(t >= tlim[0], t < tlim[1])
tidxPs = Ps[np.logical_and(Ps > tlim[0] * Fs, Ps < tlim[1] * Fs)]
tidxTs = Ts[np.logical_and(Ts > tlim[0] * Fs, Ts < tlim[1] * Fs)]
tidxDs = zeroxD[np.logical_and(zeroxD > tlim[0] * Fs, zeroxD < tlim[1] * Fs)]
tidxRs = zeroxR[np.logical_and(zeroxR > tlim[0] * Fs, zeroxR < tlim[1] * Fs)]

plt.figure(figsize=(12, 2))
plt.plot(t[tidx], signal_low[tidx], 'k')
plt.plot(t[tidxPs], signal_low[tidxPs], 'b.', ms=10)
plt.plot(t[tidxTs], signal_low[tidxTs], 'r.', ms=10)
plt.plot(t[tidxDs], signal_low[tidxDs], 'm.', ms=10)
Example #10
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
#
# 2. Localize rise and decay midpoints
# ------------------------------------
#
# In addition to localizing the peaks and troughs of a cycle, we also want to get more information
# about the rise and decay periods. For instance, these flanks may have deflections if the peaks or
# troughs are particularly sharp. In order to gauge a dimension of this, we localize midpoints for
# each of the rise and decay segments. These midpoints are defined as the times at which the voltage
# crosses halfway between the adjacent peak and trough voltages. If this threshold is crossed
# multiple times, then the median time is chosen as the flank midpoint. This is not perfect;
# however, this is rare, and most of these cycles should be removed by burst detection.
#
# Note: Plotting midpoints and extrema may also be performed using the dataframe output from
# :func:`~.compute_features` with the :func:`~.plot_cyclepoints` function.

rises, decays = find_zerox(sig_low, peaks, troughs)

plot_cyclepoints_array(sig_low,
                       fs,
                       xlim=(13, 14),
                       peaks=peaks,
                       troughs=troughs,
                       rises=rises,
                       decays=decays)

####################################################################################################
#
# 3. Compute features of each cycle
# ---------------------------------
# After these 4 points of each cycle are localized, we compute some simple statistics for each
# cycle. The main cycle-by-cycle function,  compute_features(), returns a table (pandas.DataFrame)
####################################################################################################
#
# 2. Localize rise and decay midpoints
# ------------------------------------
#
# In addition to localizing the peaks and troughs of a cycle, we also want to get more information
# about the rise and decay periods. For instance, these flanks may have deflections if the peaks or
# troughs are particularly sharp. In order to gauge a dimension of this, we localize midpoints for
# each of the rise and decay segments. These midpoints are defined as the times at which the voltage
# crosses halfway between the adjacent peak and trough voltages. If this threshold is crossed
# multiple times, then the median time is chosen as the flank midpoint. This is not perfect;
# however, this is rare, and most of these cycles should be removed by burst detection.

from bycycle.cyclepoints import find_zerox
zerox_rise, zerox_decay = find_zerox(sig_low, ps, ts)

####################################################################################################

tlim = (13, 14)
tidx = np.logical_and(times >= tlim[0], times < tlim[1])
tidx_ps = ps[np.logical_and(ps > tlim[0] * fs, ps < tlim[1] * fs)]
tidx_ts = ts[np.logical_and(ts > tlim[0] * fs, ts < tlim[1] * fs)]
tidx_ds = zerox_decay[np.logical_and(zerox_decay > tlim[0] * fs,
                                     zerox_decay < tlim[1] * fs)]
tidx_rs = zerox_rise[np.logical_and(zerox_rise > tlim[0] * fs,
                                    zerox_rise < tlim[1] * fs)]

plt.figure(figsize=(12, 2))
plt.plot(times[tidx], sig_low[tidx], 'k')
plt.plot(times[tidx_ps], sig_low[tidx_ps], 'b.', ms=10)