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
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(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
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(fs, 'fs', (0, np.inf)) check_param(start, 'start', (0, stop)) check_param(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
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(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
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(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
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(amp_fraction_threshold, 'amp_fraction_threshold', (0, 1)) check_param(amp_consistency_threshold, 'amp_consistency_threshold', (0, 1)) check_param(period_consistency_threshold, 'period_consistency_threshold', (0, 1)) check_param(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
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(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) if 'min_n_cycles' in threshold_kwargs.keys(): del threshold_kwargs['min_n_cycles'] n_kwargs = len(threshold_kwargs.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(threshold_kwargs.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] < threshold_kwargs[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, threshold_kwargs[osc_key], figsize=figsize, ax=axes[idx + 1], xlim=xlim, xlabel=xlabel, ylabel=ylabel, color=color, interp=interp)
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(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)
def compute_burst_fraction(df_samples, sig, fs, f_range, amp_threshes=(1, 2), min_n_cycles=3, 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. 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(fs, 'fs', (0, np.inf)) check_param(amp_threshes[0], 'lower amp_threshes', (0, amp_threshes[1])) check_param(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 is_burst = detect_bursts_dual_threshold(sig, fs, amp_threshes, f_range, min_n_cycles=min_n_cycles, **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
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(fs, 'fs', (0, np.inf)) # Set times and limits times = np.arange(0, len(sig) / fs, 1 / fs) xlim = (times[0], times[-1]) if xlim is None else xlim # Restrict sig and times to xlim 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 >= xlim[0]*fs) & (points < xlim[1]*fs)] cps = cps - int(xlim[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)
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(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[-1]) 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)