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