コード例 #1
0
def test_amp_by_time_consistent():
    """
    Confirm consistency in beta bandpass filter results on a neural signal
    """
    # Load data
    data_idx = 1
    x = _load_example_data(data_idx=data_idx)
    Fs = 1000
    f_range = (13, 30)

    # Load ground truth amplitude time series
    amp_true = np.load(os.path.dirname(neurodsp.__file__) + '/tests/data/sample_data_'+str(data_idx)+'_amp.npy')

    # Compute amplitude time series
    amp = neurodsp.amp_by_time(x, Fs, f_range)

    # Compute difference between current and past filtered signals
    signal_diff = amp - amp_true
    assert np.allclose(np.sum(np.abs(signal_diff)), 0, atol=10 ** -5)
コード例 #2
0
def test_NaN_in_x():
    """
    Assure that time-resolved timefrequency functions do not return all NaN
    if one of the elements in the input array is NaN.
    Do this by replacing edge artifacts with NaN for a lowpass filter
    """

    # Generate a low-pass filtered signal with NaNs
    x = np.random.randn(10000)
    Fs = 1000
    x = neurodsp.filter(x, Fs, 'lowpass', f_lo=50)

    # Compute phase, amp, and freq time series
    f_range = (4, 8)
    pha = neurodsp.phase_by_time(x, Fs, f_range)
    amp = neurodsp.amp_by_time(x, Fs, f_range)
    i_f = neurodsp.freq_by_time(x, Fs, f_range)

    assert len(pha[~np.isnan(pha)]) > 0
    assert len(amp[~np.isnan(amp)]) > 0
    assert len(i_f[~np.isnan(i_f)]) > 0
コード例 #3
0
def test_timefreq_consistent():
    """
    Confirm consistency in estimation of instantaneous phase, amp, and frequency
    with computations in previous versions
    """
    # Load data
    data_idx = 1
    x = _load_example_data(data_idx=data_idx)
    Fs = 1000
    f_range = (13, 30)

    # Load ground truth phase time series
    pha_true = np.load(
        os.path.dirname(neurodsp.__file__) + '/tests/data/sample_data_' +
        str(data_idx) + '_pha.npy')
    # Load ground truth amplitude time series
    amp_true = np.load(
        os.path.dirname(neurodsp.__file__) + '/tests/data/sample_data_' +
        str(data_idx) + '_amp.npy')
    # Load ground truth frequency time series
    i_f_true = np.load(
        os.path.dirname(neurodsp.__file__) + '/tests/data/sample_data_' +
        str(data_idx) + '_i_f.npy')

    # Compute phase time series
    pha = neurodsp.phase_by_time(x, Fs, f_range)
    # Compute amplitude time series
    amp = neurodsp.amp_by_time(x, Fs, f_range)
    # Compute frequency time series
    i_f = neurodsp.freq_by_time(x, Fs, f_range)

    # Compute difference between current and past signals
    assert np.allclose(np.sum(np.abs(pha - pha_true)), 0, atol=10**-5)
    assert np.allclose(np.sum(np.abs(amp - amp_true)), 0, atol=10**-5)
    assert np.allclose(np.sum(
        np.abs(i_f[~np.isnan(i_f)] - i_f_true[~np.isnan(i_f_true)])),
                       0,
                       atol=10**-5)
コード例 #4
0
ファイル: burst.py プロジェクト: konoske/neurodsp
def detect_bursts(x,
                  Fs,
                  f_range,
                  algorithm,
                  min_osc_periods=3,
                  dual_thresh=None,
                  deviation_type='median',
                  magnitude_type='amplitude',
                  return_amplitude=False,
                  filter_fn=None,
                  filter_kwargs=None):
    """
    Detect periods of oscillatory bursting in a neural signal

    Parameters
    ----------
    x : array-like 1d
        voltage time series
    Fs : float
        The sampling rate in Hz
    f_range : (low, high), Hz
        NOTE: Not relevant in the 'bosc' method
        frequency range for narrowband signal of interest
    algorithm : string
        Name of algorithm to be used.
        'deviation' : uses multiple of amplitude in frequency range like in
                      Feingold et al., 2015 (esp. Fig. 4)
        'fixed_thresh' : uses a given threshold in the same units as 'magnitude'
                         parameter
    min_osc_periods : float
        minimum burst duration in terms of number of cycles of f_range[0]
    dual_thresh : (low, high), units depend on other parameters
        NOTE: Only used when algorithm = 'deviation' or 'fixed_thresh'
        Threshold values for determining burst
    deviation_type : string in ('median', 'mean')
        NOTE: Only used when algorithm = 'deviation' or 'fixed_thresh'
        metric to normalize magnitude used for thresholding
    magnitude_type : string in ('power', 'amplitude')
        NOTE: Only used when algorithm = 'deviation' or 'fixed_thresh'
        metric of magnitude used for thresholding
    filter_fn : filter function with required inputs (x, f_range, Fs, rmv_edge)
        NOTE: Only used when algorithm = 'deviation' or 'fixed_thresh'
        function to use to filter original time series, x
    filter_kwargs : dict
        NOTE: Only used when algorithm = 'deviation' or 'fixed_thresh'
        keyword arguments to the filter_fn
    """

    if algorithm in ['deviation', 'fixed_thresh']:

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

        # Assure dual_thresh has input
        if dual_thresh is None:
            raise ValueError(
                'Need to specify dual magnitude thresholds for this algorithm')

        # Process deviation_type kwarg
        if deviation_type not in ['median', 'mean']:
            raise ValueError(
                "Invalid 'baseline' parameter. Must be 'median' or 'mean'")

        # Compute amplitude time series
        x_amplitude = amp_by_time(x,
                                  Fs,
                                  f_range,
                                  filter_fn=filt.filter,
                                  filter_kwargs=filter_kwargs)

        # Set magnitude as power or amplitude
        if magnitude_type == 'power':
            x_magnitude = x_amplitude**2  # np.power faster?
        elif magnitude_type == 'amplitude':
            x_magnitude = x_amplitude
        else:
            raise ValueError("Invalid 'magnitude' parameter")

        # Rescale magnitude by median or mean
        # If 'fixed_thresh', x_magnitude is unchanged
        if algorithm == 'deviation':
            # Calculate normalized magnitude
            if deviation_type == 'median':
                x_magnitude = x_magnitude / np.median(x_magnitude)
            elif deviation_type == 'mean':
                x_magnitude = x_magnitude / np.mean(x_magnitude)

        if len(dual_thresh) != 2:
            raise ValueError(
                "Invalid number of elements in 'dual_thresh' parameter")

        # Identify time periods of oscillation using the 2 thresholds
        isosc = _2threshold_split(x_magnitude, dual_thresh[1], dual_thresh[0])

    else:
        raise ValueError("Invalid 'algorithm' parameter")

    # Remove short time periods of oscillation
    min_period_length = int(np.ceil(min_osc_periods * Fs / f_range[0]))
    isosc_noshort = _rmv_short_periods(isosc, min_period_length)

    if return_amplitude:
        return isosc_noshort, x_magnitude
    else:
        return isosc_noshort
コード例 #5
0
ファイル: pac.py プロジェクト: GrantRVD/neurodsp
def compute_pac_comodulogram(x_pha,
                             x_amp,
                             Fs,
                             f_pha_bin_edges,
                             f_amp_bin_edges,
                             N_cycles_pha=None,
                             N_cycles_amp=None,
                             filter_fn=None,
                             filter_kwargs=None,
                             hilbert_increase_N=False,
                             pac_method='ozkurt',
                             N_bins_tort=None,
                             N_surr_canolty=None,
                             verbose=True):
    """
    Calculate phase-amplitude coupling between a low-frequency
    range of x_pha and a higher frequency range in x_amp

    Parameters
    ----------
    x_pha : array-like, 1d
        The time-series from which to compute the phase component
    x_amp : array-like, 1d
        The time series from which to compute the amplitude component
    Fs : float
        Sampling rate (Hz) of the two time series
    f_pha_bin_edges: array-like, 1d
        An array of frequency values (Hz) that define the edges of the
        frequency ranges on which to estimate phase
    f_amp_bin_edges : array-like, 1d
        An array of frequency values (Hz) that define the edges of the
        frequency ranges on which to estimate amplitude
    N_cycles_pha : float, optional
        Length of the low band-pass filter in terms of the number of cycles
        of a sine wave with a frequency at the low-cutoff of the bandpass filter
    N_cycles_amp : float, optional
        Length of the high band-pass filter in terms of the number of cycles
        of a sine wave with a frequency at the low-cutoff of the bandpass filter
    filter_fn : function or False, optional
        The filtering function, with api:
        `filterfn(x, Fs, pass_type, f_lo, f_hi, remove_edge_artifacts=True)
        If False, it is assumed that x_pha and x_amp are the phase time
        series and the amplitude time series, respectively. Therefore, no
        filtering or hilbert transform will be done.
    filter_kwargs : dict, optional
        Keyword parameters to pass to `filterfn(.)`
    hilbert_increase_N : bool, optional
        if True, zeropad the signal to length the next power of 2 when doing the hilbert transform.
        This is because scipy.signal.hilbert can be very slow for some lengths of x
    pac_method : {'ozkurt', 'plv', 'glm', 'tort', 'canolty'}, optional
        Indicates the method used to correlate the phase and amplitude time series in order to
        quantify the strength of pac.
        'ozkurt' : normalized modulation index method (see Ozkurt & Schnitzler, 2011, J Neuro Methods)
        'plv': phase-locking value method (see Penny et al., 2008, J Neuro Methods)
        'glm': general linear model method (see Penny et al., 2008, J Neuro Methods)
        'tort': modulation index method (see Tort et al., 2010, J Neurophys)
        'canolty' : modulation index method (see Canolty et al., 2006, Science)
    N_bins_tort : int or None, optional
        Number of phase bins to use in Tort's modulation index method of estimating PAC
    N_surr_canolty : int or None, optional
        Number of surrograte runs for Canolty's modulation index method of estimating PAC
    verbose : bool, optional
        if True, print optional warning information

    Returns
    -------
    pac : 2d array
        phase-amplitude coupling strength values for each combination of phase-providing
        frequency bin and amplitude-providing frequency bin.
    """

    # Display warning about the true width of frequency bins
    if verbose:
        warnings.warn(
            "The true bandwidth of the filters used for each frequency bin of the comodulogram "
            "is almost always are wider than the declared width of the frequency bin. "
            "And this width increases as a function of frequency."
            "For example the frequency bin 60-64Hz likely uses a bandwidth >4Hz. "
            "You can decrease this bandwidth by increasing the N_cycles_pha and N_cycles_amp arguments. "
            "This warning can be turned off by setting the 'verbose' kwarg to False."
        )

    # Set default filtering parameters
    if filter_fn is None:
        filter_fn = neurodsp.filter
    if filter_kwargs is None:
        filter_kwargs_pha = {'N_cycles': N_cycles_pha, 'verbose': False}
        filter_kwargs_amp = {'N_cycles': N_cycles_amp, 'verbose': False}
    else:
        filter_kwargs_pha['N_cycles'] = N_cycles_pha
        filter_kwargs_pha['verbose'] = False
        filter_kwargs_amp['N_cycles'] = N_cycles_amp
        filter_kwargs_amp['verbose'] = False

    # Compute phase time series for each frequency bin
    N_bins_pha = len(f_pha_bin_edges) - 1
    pha_by_bin = np.zeros((N_bins_pha, len(x_pha)))
    for i in range(N_bins_pha):
        f_range_temp = (f_pha_bin_edges[i], f_pha_bin_edges[i + 1])
        pha_by_bin[i] = neurodsp.phase_by_time(x_pha,
                                               Fs,
                                               f_range_temp,
                                               filter_fn=filter_fn,
                                               filter_kwargs=filter_kwargs_pha,
                                               hilbert_increase_N=False)

    # Compute amplitude time series for each frequency bin
    N_bins_amp = len(f_amp_bin_edges) - 1
    amp_by_bin = np.zeros((N_bins_amp, len(x_pha)))
    for i in range(N_bins_amp):
        f_range_temp = (f_amp_bin_edges[i], f_amp_bin_edges[i + 1])
        amp_by_bin[i] = neurodsp.amp_by_time(x_amp,
                                             Fs,
                                             f_range_temp,
                                             filter_fn=filter_fn,
                                             filter_kwargs=filter_kwargs_amp,
                                             hilbert_increase_N=False)

    # For each pair of frequency bins, compute PAC
    pac = np.zeros((N_bins_pha, N_bins_amp))
    for i in range(N_bins_pha):
        for j in range(N_bins_amp):
            f_range_pha_temp = (f_pha_bin_edges[i], f_pha_bin_edges[i + 1])
            f_range_amp_temp = (f_amp_bin_edges[j], f_amp_bin_edges[j + 1])
            pac[i, j] = compute_pac(pha_by_bin[i],
                                    amp_by_bin[j],
                                    Fs,
                                    f_range_pha_temp,
                                    f_range_amp_temp,
                                    filter_fn=False,
                                    pac_method=pac_method,
                                    N_bins_tort=N_bins_tort,
                                    N_surr_canolty=N_surr_canolty,
                                    verbose=False)
    return pac
コード例 #6
0
ファイル: pac.py プロジェクト: GrantRVD/neurodsp
def compute_pac(x_pha,
                x_amp,
                Fs,
                f_range_lo,
                f_range_hi,
                N_seconds_lo=None,
                N_seconds_hi=None,
                filter_fn=None,
                filter_kwargs=None,
                hilbert_increase_N=False,
                pac_method='ozkurt',
                N_bins_tort=None,
                N_surr_canolty=None,
                verbose=True):
    """
    Calculate phase-amplitude coupling between a low-frequency
    range of x_pha and a higher frequency range in x_amp

    Parameters
    ----------
    x_pha : array-like, 1d
        The time-series from which to compute the phase component
    x_amp : array-like, 1d
        The time series from which to compute the amplitude component
    Fs : float
        Sampling rate (Hz) of the two time series
    f_range_lo : tuple, 2 elements
        The low frequency filtering range (Hz)
    f_range_hi : tuple, 2 elements
        The high frequency filtering range (Hz)
    N_seconds_lo : float, optional
        Length of the low band-pass filter (seconds)
    N_seconds_hi : float, optional
        Length of the high band-pass filter (seconds)
    filter_fn : function or None, optional
        The filtering function, with api:
        `filterfn(x, Fs, pass_type, f_lo, f_hi, remove_edge_artifacts=True)
        If False, it is assumed that x_pha and x_amp are the phase time
        series and the amplitude time series, respectively. Therefore, no
        filtering or hilbert transform will be done.
    filter_kwargs : dict, optional
        Keyword parameters to pass to `filterfn(.)`
    hilbert_increase_N : bool, optional
        if True, zeropad the signal to length the next power of 2 when doing the hilbert transform.
        This is because scipy.signal.hilbert can be very slow for some lengths of x
    pac_method : {'ozkurt', 'plv', 'glm', 'tort', 'canolty'}, optional
        Indicates the method used to correlate the phase and amplitude time series in order to
        quantify the strength of pac.
        'ozkurt' : normalized modulation index method (see Ozkurt & Schnitzler, 2011, J Neuro Methods)
        'plv': phase-locking value method (see Penny et al., 2008, J Neuro Methods)
        'glm': general linear model method (see Penny et al., 2008, J Neuro Methods)
        'tort': modulation index method (see Tort et al., 2010, J Neurophys)
        'canolty' : modulation index method (see Canolty et al., 2006, Science)
    N_bins_tort : int or None, optional
        Number of phase bins to use in Tort's modulation index method of estimating PAC
    N_surr_canolty : int or None, optional
        Number of surrograte runs for Canolty's modulation index method of estimating PAC
    verbose : bool, optional
        if True, print optional warning information

    Returns
    -------
    pac : float
        phase-amplitude coupling strength
    """
    # Set default filtering parameters
    if N_seconds_lo is None:
        if verbose:
            warnings.warn(
                'Filter order not specified. Filter length automatically set to 3 cycles of the low cutoff frequency.'
            )
        N_cycles = 3
        N_seconds_lo = N_cycles / f_range_lo[0]
    if N_seconds_hi is None:
        if verbose:
            warnings.warn(
                'Filter order not specified. Filter length automatically set to 3 cycles of the low cutoff frequency.'
            )
        N_cycles = 3
        N_seconds_hi = N_cycles / f_range_hi[0]
    if filter_fn is None:
        filter_fn = neurodsp.filter
    if filter_kwargs is None:
        filter_kwargs = {}

    # Only compute phase and amplitude if filter_fn is not False
    if filter_fn is not False:
        # Compute phase time series
        filter_kwargs['N_seconds'] = N_seconds_lo
        filter_kwargs['verbose'] = verbose
        pha = neurodsp.phase_by_time(x_pha,
                                     Fs,
                                     f_range_lo,
                                     filter_fn=filter_fn,
                                     filter_kwargs=filter_kwargs,
                                     hilbert_increase_N=hilbert_increase_N)

        # Compute amp time series
        filter_kwargs['N_seconds'] = N_seconds_hi
        amp = neurodsp.amp_by_time(x_amp,
                                   Fs,
                                   f_range_hi,
                                   filter_fn=filter_fn,
                                   filter_kwargs=filter_kwargs,
                                   hilbert_increase_N=hilbert_increase_N)
    else:
        # Set phase and amplitude time series to 'x' if filter_fn set to False
        pha = x_pha
        amp = x_amp

        # Reset filter function and kwargs
        filter_fn = neurodsp.filter
        filter_kwargs = {'verbose': verbose}

    # Remove the part of both signals with edge artifacts
    # The filter should be longer for the lower-frequency phase-providing
    # signal
    first_nonan = np.where(~np.isnan(pha))[0][0]
    last_nonan = np.where(~np.isnan(pha))[0][-1] + 1
    pha_nonan = pha[first_nonan:last_nonan]
    amp_nonan = amp[first_nonan:last_nonan]

    # Compute statistic relating phase and amplitude
    if pac_method == 'plv':
        pac = _plv_pac(pha, amp, Fs, f_range_lo, N_seconds_lo, filter_fn,
                       filter_kwargs, hilbert_increase_N)
    elif pac_method == 'glm':
        pac = _glm_pac(pha, amp)
    elif pac_method == 'tort':
        pac = _tort_pac(pha, amp, N_bins_tort)
    elif pac_method == 'canolty':
        pac = _canolty_pac(pha, amp, N_surr_canolty)
    elif pac_method == 'ozkurt':
        pac = _ozkurt_pac(pha, amp)
    else:
        raise ValueError('Method specified in "pac_method" not known.')
    return pac
コード例 #7
0
def features_by_cycle(x,
                      Fs,
                      f_range,
                      center_extrema='P',
                      find_extrema_kwargs=None,
                      estimate_oscillating_periods=False,
                      estimate_oscillating_periods_kwargs=None,
                      hilbert_increase_N=False):
    """
    Calculate several features of an oscillation's waveform
    shape for each cycle in a recording.

    Parameters
    ----------
    x : array-like 1d
        voltage time series
    Fs : float
        sampling rate (Hz)
    f_range : (low, high), Hz
        frequency range for narrowband signal of interest
    center_extrema : str
        The center extrema in the cycle
        'P' : cycles are defined trough-to-trough
        'T' : cycles are defined peak-to-peak
    find_extrema_kwargs : dict or None
        Keyword arguments for function to find peaks and
        troughs (find_extrema) to change filter
        parameters or boundary
    estimate_oscillating_periods: bool
        if True, call _define_true_oscillating_periods to
        declare each cycle as in an oscillating or not.
    estimate_oscillating_periods_kwargs : dict or None
        Keyword arguments for function to find label cycles
        as in or not in an oscillation
    hilbert_increase_N : bool
        corresponding kwarg for neurodsp.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 oscillatory cycle. Each row is one cycle.
        Note that columns are slightly different depending on if
        'center_extrema' is set to 'P' or 'T'.
        Each column is described below for peak-centered cycles,
        but are similar for trough-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
        volt_rdsym : voltage difference between rise and decay
        time_rdsym : fraction of cycle in the rise period
        volt_ptsym : voltage difference between peak and trough
        time_ptsym : fraction of cycle in the peak period
        oscillator_amplitude : average amplitude of the oscillation in that
                               frequency band during the cycle

    Notes
    -----
    * 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.
    """

    # Set defaults if user input is None
    if estimate_oscillating_periods_kwargs is None:
        estimate_oscillating_periods_kwargs = {}
    if find_extrema_kwargs is None:
        find_extrema_kwargs = {}
    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.')

    # 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 = neurodsp.shape.find_extrema(x, Fs, f_range, **find_extrema_kwargs)

    # Find zero-crossings
    zeroxR, zeroxD = neurodsp.shape.find_zerox(x, Ps, Ts)

    # Determine number of cycles
    N_t2t = len(Ts) - 1

    # 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
    half_decay_time = (zeroxD[1:] - Ps[1:])
    half_rise_time = (Ps[1:] - zeroxR)
    shape_features['time_peak'] = half_decay_time + half_rise_time

    # Compute duration of last trough
    half_decay_time = (Ts[:-1] - zeroxD[:-1])
    half_rise_time = (zeroxR - Ts[:-1])
    shape_features['time_trough'] = half_decay_time + half_rise_time

    # 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['volt_rdsym'] = shape_features[
        'volt_rise'] - shape_features['volt_decay']
    shape_features[
        'time_rdsym'] = shape_features['time_rise'] / shape_features['period']

    # Compute peak-trough symmetry features
    shape_features['volt_ptsym'] = shape_features[
        'volt_peak'] + shape_features['volt_trough']
    shape_features['time_ptsym'] = shape_features['time_peak'] / (
        shape_features['time_peak'] + shape_features['time_trough'])

    # Compute average oscillatory amplitude estimate during cycle
    amp = neurodsp.amp_by_time(x,
                               Fs,
                               f_range,
                               hilbert_increase_N=hilbert_increase_N)
    shape_features['oscillator_amplitude'] = [
        np.mean(amp[Ts[i]:Ts[i + 1]]) for i in range(N_t2t)
    ]

    # Convert feature dictionary into a DataFrame
    df = pd.DataFrame.from_dict(shape_features)

    # 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['volt_rdsym'] = -df['volt_rdsym']
        df['volt_ptsym'] = -df['volt_ptsym']
        df['time_rdsym'] = 1 - df['time_rdsym']
        df['time_ptsym'] = 1 - df['time_ptsym']

    # Define whether or not each cycle is part of an oscillation
    if estimate_oscillating_periods:
        if center_extrema == 'T':
            x = -x
        df = define_true_oscillating_periods(
            df, x, **estimate_oscillating_periods_kwargs)

    return df