def test_plot_instantaneous_measure(tsig_comb): times = create_times(N_SECONDS, FS) plot_instantaneous_measure( times, amp_by_time(tsig_comb, FS, F_RANGE), 'amplitude', save_fig=True, file_path=TEST_PLOTS_PATH, file_name='test_plot_instantaneous_measure_amplitude.png') plot_instantaneous_measure( times, phase_by_time(tsig_comb, FS, F_RANGE), 'phase', save_fig=True, file_path=TEST_PLOTS_PATH, file_name='test_plot_instantaneous_measure_phase.png') plot_instantaneous_measure( times, freq_by_time(tsig_comb, FS, F_RANGE), 'frequency', save_fig=True, file_path=TEST_PLOTS_PATH, file_name='test_plot_instantaneous_measure_frequency.png') # Check the error for bad measure with raises(ValueError): plot_instantaneous_measure(times, tsig_comb, 'BAD')
def _compute_bandpowers(self, eegdata): deltaFreq = (2, 4) thetaFreq = (4, 8) alphaFreq = (8, 12) betaFreq = (12, 30) toAdd = dict() for channel in Channel: channelIndex = channel.value numToPad = 5 padded_data = [ 0 if not (i >= numToPad and i < (len(eegdata[channelIndex]) + numToPad)) else eegdata[channelIndex][i - numToPad] for i in range(len(eegdata[channelIndex]) + (numToPad * 2)) ] padded_data = np.asarray(padded_data) toAdd[str(Band.DELTA.name + "_" + channel.name)] = np.nanmean( timefrequency.amp_by_time(padded_data, self.fs, f_range=deltaFreq, verbose=False, filter_kwargs={"n_seconds": 1})) toAdd[str(Band.THETA.name + "_" + channel.name)] = np.nanmean( timefrequency.amp_by_time(padded_data, self.fs, f_range=thetaFreq, verbose=False, filter_kwargs={"n_seconds": 1})) toAdd[str(Band.ALPHA.name + "_" + channel.name)] = np.nanmean( timefrequency.amp_by_time(padded_data, self.fs, f_range=alphaFreq, verbose=False, filter_kwargs={"n_seconds": 1})) toAdd[str(Band.BETA.name + "_" + channel.name)] = np.nanmean( timefrequency.amp_by_time(padded_data, self.fs, f_range=betaFreq, verbose=False, filter_kwargs={"n_seconds": 1})) return toAdd
def bootstrap_xcorr(sig1, sig2, fs, f_range, low_shift=5, high_shift=10, n_shifts=1000): """ Bootstrapping resampling of crosscorrelations by temporally shifting one signal 5-10 seconds forward or backwards relative to the other. """ # generate random array of seconds to shift shift_seconds = rand_neg_uni(low_shift, high_shift, n_shifts) # Get bandpass amplitude via hilbert transform amp1 = amp_by_time(sig1, fs, f_range, remove_edges=False) amp1 = amp1 - np.mean(amp1) amp2 = amp_by_time(sig2, fs, f_range, remove_edges=False) amp2 = amp2 - np.mean(amp2) # computer xcorr for every shift bs_dist = np.zeros(shift_seconds.shape) for n in range(0, len(shift_seconds)): # shift signal shift_samples = int(round(shift_seconds[n] * fs)) # seconds to samples amp2_shifted = np.roll(amp2, shift_samples) # compute cross-correlation, convert lag to ms lags, crosscorr = xcorr(amp1, amp2_shifted, maxlags=round(fs / 10)) lags = (lags / fs) * 1000 # get max xcorr bs_dist[n] = crosscorr.max() return bs_dist
def compute_band_amp(df_samples, sig, fs, f_range, n_cycles=3): """Compute the average amplitude of each oscillation. 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). n_cycles : int, optional, default: 3 Length of filter, in number of cycles, at the lower cutoff frequency. Returns ------- band_amp : 1d array Average analytic amplitude of the oscillation. Examples -------- Compute the mean amplitude for each cycle: >>> 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)) >>> band_amp = compute_band_amp(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(n_cycles, 'n_cycles', (0, np.inf)) amp = amp_by_time(sig, fs, f_range, remove_edges=False, n_cycles=n_cycles) troughs = np.append(df_samples['sample_last_trough'].values[0], df_samples['sample_next_trough'].values) band_amp = [ np.mean(amp[troughs[sig_idx]:troughs[sig_idx + 1]]) for sig_idx in range(len(df_samples['sample_peak'])) ] return band_amp
plot_instantaneous_measure(times, pha, xlim=[4, 5], ax=axs[1]) ################################################################################################### # Instantaneous Amplitude # ----------------------- # # Instantaneous amplitude is a measure of the amplitude of a signal, over time. # # Instantaneous amplitude can be analyzed with the # :func:`~neurodsp.timefrequency.hilbert.amp_by_time` function. # ################################################################################################### # Compute instantaneous amplitude from a signal amp = amp_by_time(sig, fs, f_range) ################################################################################################### # Plot example signal _, axs = plt.subplots(2, 1, figsize=(15, 6)) plot_instantaneous_measure(times, [sig, amp], 'amplitude', labels=['Raw Voltage', 'Amplitude'], xlim=[4, 5], xlabel=None, ax=axs[0]) plot_instantaneous_measure(times, [sig_filt_true, amp], 'amplitude', labels=['Raw Voltage', 'Amplitude'], colors=['b', 'r'],
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
def amp_xcorr(sig1, sig2, fs, f_range, plot=True): """Filters two signals between a specified freq band, calculates the crosscorrelation of the amplitude envelope of the filter signals, and returns the max crosscorrelation and corresponding lag. Parameters ---------- sig1 : 1d array local field potential 1 sig2 : 1d array local field potential 2 fs : float sampling frequency in Hz f_range : tuple float filter range in Hz as [low, high] Returns ------- max_crosscorr : float maximum crosscorrelation of the two signals max_crosscorr_lag : float lag between the signals at the max crosscorrelation Notes ----- This code was adapted from the amp_crosscorr() Matlab function written by Adhikari et al. [1]. This uses both the xcorr() python function to compute crosscorrelations written by github user colizoli [2] and amp_by_time() function written by Cole et al. [3]. References ---------- [1] Adhikari, A., Sigurdsson, T., Topiwala, M.A. and Gordon, J.A. 2010. Journal of Neuroscience Methods 191(2), pp. 191–200. DIO: 10.1016/j.jneumeth.2010.06.019 [2] https://github.com/colizoli/xcorr_python [3] Cole, S., Donoghue, T., Gao, R., & Voytek, B. (2019). NeuroDSP: A package for neural digital signal processing. Journal of Open Source Software, 4(36), 1272. DOI: 10.21105/joss.01272 """ # Get bandpass amplitude via hilbert transform amp1 = amp_by_time(sig1, fs, f_range, remove_edges=False) amp1 = amp1 - np.mean(amp1) amp2 = amp_by_time(sig2, fs, f_range, remove_edges=False) amp2 = amp2 - np.mean(amp2) # compute cross-correlation, convert lag to ms lags, crosscorr = xcorr(amp1, amp2, maxlags=round(fs / 10)) lags = (lags / fs) * 1000 # get max xcorr and lag g = crosscorr.argmax() max_crosscorr = crosscorr[g] max_crosscorr_lag = lags[g] # plot if plot: plt.plot(lags, crosscorr) plt.scatter(lags[g], crosscorr[g], s=50, color='r', marker='*') plt.axvline(x=0, color='k', linestyle='--') plt.xlim([-100, 100]) plt.xlabel('Lag (ms)') plt.ylabel('Cross-correlation') return max_crosscorr, max_crosscorr_lag
# Simulation settings n_seconds = 10 fs = 1000 components = {'sim_bursty_oscillation': {'freq': 10, 'enter_burst': .1, 'leave_burst': .1, 'cycle': 'asine', 'rdsym': 0.3}, 'sim_powerlaw': {'f_range': (2, None)}} sig = sim_combined(n_seconds, fs, components=components, component_variances=(2, 1)) # Filter settings f_alpha = (8, 12) n_seconds_filter = .5 # Compute amplitude and phase sig_filt = filter_signal(sig, fs, 'bandpass', f_alpha, n_seconds=n_seconds_filter) theta_amp = amp_by_time(sig, fs, f_alpha, n_seconds=n_seconds_filter) theta_phase = phase_by_time(sig, fs, f_alpha, n_seconds=n_seconds_filter) # Plot signal times = create_times(n_seconds, fs) xlim = (2, 6) tidx = np.logical_and(times >= xlim[0], times < xlim[1]) fig, axes = plt.subplots(figsize=(15, 9), nrows=3) # Plot the raw signal plot_time_series(times[tidx], sig[tidx], ax=axes[0], ylabel='Voltage (mV)', xlabel='', lw=2, labels='raw signal') # Plot the filtered signal and oscillation amplitude plot_instantaneous_measure(times[tidx], [sig_filt[tidx], theta_amp[tidx]],
def amplitude_envelope(time_series, Fs, filter_range): envelope = amp_by_time(time_series, Fs, filter_range) return np.nanmean(envelope)
def get_inst_data(data, fs, f_range): i_phase = tf.phase_by_time(data, fs, f_range) i_amp = tf.amp_by_time(data, fs, f_range) i_freq = tf.freq_by_time(data, fs, f_range) return i_phase, i_amp, i_freq
def get_alpha_instantaneous_statistics(eeg_epoch_full_df): # alpha_range = (7, 12) alpha_amps = {} alpha_pha = {} alpha_if = {} power_range = [(4, 7), (7, 12), (12, 30)] for power_i in range(len(power_range)): alpha_range = power_range[power_i] for i in range(0, len(eeg_epoch_full_df)): for ch in all_chans: sig = eeg_epoch_full_df[ch][i][:] key = ch + "_" + str(alpha_range) amp = amp_by_time( sig, eeg_fs, alpha_range) # Amplitude by time (instantaneous amplitude) if key + "_amp_med" not in alpha_amps: alpha_amps[key + "_amp_med"] = list() alpha_amps[key + "_amp_avg"] = list() alpha_amps[key + "_amp_std"] = list() alpha_amps[key + "_amp_gradmax"] = list() alpha_amps[key + "_amp_gradmin"] = list() alpha_amps[key + "_amp_med"].append(np.nanmedian(amp)) alpha_amps[key + "_amp_avg"].append(np.nanmean(amp)) alpha_amps[key + "_amp_std"].append(np.nanstd(amp)) alpha_amps[key + "_amp_gradmax"].append( np.nanmax(np.gradient(amp))) alpha_amps[key + "_amp_gradmin"].append( np.nanmin(np.gradient(amp))) pha = phase_by_time( sig, eeg_fs, alpha_range) # Phase by time (instantaneous phase) if key + "_pha_med" not in alpha_pha: alpha_pha[key + "_pha_med"] = list() alpha_pha[key + "_pha_avg"] = list() alpha_pha[key + "_pha_std"] = list() alpha_pha[key + "_pha_gradmax"] = list() alpha_pha[key + "_pha_gradmin"] = list() alpha_pha[key + "_pha_med"].append(np.nanmedian(pha)) alpha_pha[key + "_pha_avg"].append(np.nanmean(pha)) alpha_pha[key + "_pha_std"].append(np.nanstd(pha)) alpha_pha[key + "_pha_gradmax"].append( np.nanmax(np.gradient(pha))) alpha_pha[key + "_pha_gradmin"].append( np.nanmin(np.gradient(pha))) i_f = freq_by_time( sig, eeg_fs, alpha_range) # Frequency by time (instantaneous frequency) if key + "_freq_med" not in alpha_if: alpha_if[key + "_freq_med"] = list() alpha_if[key + "_freq_avg"] = list() alpha_if[key + "_freq_std"] = list() alpha_if[key + "_freq_gradmax"] = list() alpha_if[key + "_freq_gradmin"] = list() alpha_if[key + "_freq_med"].append(np.nanmedian(i_f)) alpha_if[key + "_freq_avg"].append(np.nanmean(i_f)) alpha_if[key + "_freq_std"].append(np.nanstd(i_f)) alpha_if[key + "_freq_gradmax"].append( np.nanmax(np.gradient(i_f))) alpha_if[key + "_freq_gradmin"].append( np.nanmin(np.gradient(i_f))) alpha_med_df = pd.DataFrame(alpha_amps) alpha_pha_df = pd.DataFrame(alpha_pha) alpha_if_df = pd.DataFrame(alpha_if) insta_stat_df = pd.concat([alpha_med_df, alpha_pha_df, alpha_if_df], axis=1) print(list(insta_stat_df.columns)) return insta_stat_df
times = create_times(len(sig) / fs, fs) times = times[0:len(times) - 1] #%% Calculate and plot # filter data on the providing bands phase_filt_signal = filter_signal(data[:, ch], fs, 'bandpass', phase_providing_band) ampl_filt_signal = filter_signal(data[:, ch], fs, 'bandpass', amplitude_providing_band) # calculate the phase phase_signal = phase_by_time(data[:, ch], fs, phase_providing_band) # Compute instaneous amplitude from a signal amp_signal = amp_by_time(sig, fs, amplitude_providing_band) #%% Plot the phase # plot of phase: raw data, filtered, and phase _, axs = plt.subplots(3, 1, figsize=(15, 6)) plot_time_series(times, data[:, ch], xlim=plt_time, xlabel=None, ax=axs[0]) plot_time_series(times, phase_filt_signal, xlim=plt_time, xlabel=None, ax=axs[1]) plot_instantaneous_measure(times, phase_signal, xlim=plt_time, ax=axs[2]) #%% Plot the amplitudes