Source code for tensorpac.methods.meth_pac_nb

"""Numba implementation of some PAC functions."""
import numpy as np
from scipy.special import erfinv


# if Numba not installed, this section should return a Numba-free jit wrapper
try:
    import numba
    def jit(signature=None, nopython=True, nogil=True, fastmath=True,  # noqa
            cache=True, **kwargs):
        return numba.jit(signature_or_function=signature, cache=cache,
                         nogil=nogil, fastmath=fastmath, nopython=nopython,
                         **kwargs)
except:
    def jit(*args, **kwargs):  # noqa
        def _jit(func):
            return func
        return _jit


[docs]@jit("f8[:,:,:](f8[:,:,:], f8[:,:,:])") def mean_vector_length_nb(pha, amp): """Numba-based Mean Vector Length (MVL). Parameters ---------- pha, amp : array_like Respectively the arrays of phases of shape (n_pha, n_epochs, n_times) and the array of amplitudes of shape (n_amp, n_epochs, n_times). Both arrays should be of type float64 (np.float64) Returns ------- pac : array_like Array of phase amplitude coupling of shape (n_amp, n_pha, n_epochs) References ---------- Canolty et al. 2006 :cite:`canolty2006high` """ n_pha, n_epochs, n_times = pha.shape n_amp, _, _ = amp.shape pac = np.zeros((n_amp, n_pha, n_epochs), dtype=np.float64) # single conversion exp_pha = np.exp(1j * pha) amp_comp = amp.astype(np.complex128) for a in range(n_amp): for p in range(n_pha): for tr in range(n_epochs): _pha = np.ascontiguousarray(exp_pha[p, tr, :]) _amp = np.ascontiguousarray(amp_comp[a, tr, :]) pac[a, p, tr] = abs(np.dot(_amp, _pha)) pac /= n_times return pac
@jit("f8[:](f8[:], f8[:], u8, b1)") def _kl_hr_nb(pha, amp, n_bins=18, mean_bins=True): """Binarize the amplitude according to phase values. This function is shared by the Kullback-Leibler Distance and the Height Ratio. """ vecbin = np.linspace(-np.pi, np.pi, n_bins + 1) phad = np.digitize(pha, vecbin) - 1 u_phad = np.unique(phad) abin = np.zeros((len(u_phad)), dtype=np.float64) for n_i, i in enumerate(u_phad): # find where phase take vecbin values idx = np.ascontiguousarray((phad == i).astype(np.float64)) m = idx.sum() if mean_bins else 1. # take the sum of amplitude inside the bin abin[n_i] = np.dot(np.ascontiguousarray(amp), idx) / m return abin
[docs]@jit("f8[:,:,:](f8[:,:,:], f8[:,:,:], u8)") def modulation_index_nb(pha, amp, n_bins=18): """Numba-based Modulation index (MI). The modulation index is obtained using the Kullback Leibler Distance which measures how much the distribution of binned amplitude differs from a uniform distribution. Parameters ---------- pha, amp : array_like Respectively the arrays of phases of shape (n_pha, n_epochs, n_times) and the array of amplitudes of shape (n_amp, n_epochs, n_times). Both arrays should be of type float64 (np.float64) n_bins : int | 18 Number of bins to binarize the amplitude according to phase intervals (should be np.int64) Returns ------- pac : array_like Array of phase amplitude coupling of shape (n_amp, n_pha, ...) References ---------- Tort et al. 2010 :cite:`tort2010measuring` """ n_pha, n_epochs, n_times = pha.shape n_amp, _, _ = amp.shape pac = np.zeros((n_amp, n_pha, n_epochs), dtype=np.float64) bin_log = np.log(n_bins) for a in range(n_amp): for p in range(n_pha): for tr in range(n_epochs): # select phase and amplitude _pha = np.ascontiguousarray(pha[p, tr, :]) _amp = np.ascontiguousarray(amp[a, tr, :]) # get the probability of each amp bin p_j = _kl_hr_nb(_pha, _amp, n_bins=n_bins, mean_bins=True) p_j /= p_j.sum() # log it (only if strictly positive) if np.all(p_j > 0.): p_j *= np.log(p_j) # compute the PAC pac[a, p, tr] = 1. + p_j.sum() / bin_log else: pac[a, p, tr] = 0. return pac
[docs]@jit("f8[:,:,:](f8[:,:,:], f8[:,:,:], u8)") def heights_ratio_nb(pha, amp, n_bins=18): """Numba-based Heights ratio (HR). Parameters ---------- pha, amp : array_like Respectively the arrays of phases of shape (n_pha, n_epochs, n_times) and the array of amplitudes of shape (n_amp, n_epochs, n_times). Both arrays should be of type float64 (np.float64) n_bins : int | 18 Number of bins to binarize the amplitude according to phase intervals (should be np.int64) Returns ------- pac : array_like Array of phase amplitude coupling of shape (n_amp, n_pha, ...) References ---------- Lakatos et al. 2005 :cite:`lakatos2005oscillatory` """ n_pha, n_epochs, n_times = pha.shape n_amp, _, _ = amp.shape pac = np.zeros((n_amp, n_pha, n_epochs), dtype=np.float64) for a in range(n_amp): for p in range(n_pha): for tr in range(n_epochs): # select phase and amplitude _pha = np.ascontiguousarray(pha[p, tr, :]) _amp = np.ascontiguousarray(amp[a, tr, :]) # get the probability of each amp bin p_j = _kl_hr_nb(_pha, _amp, n_bins=n_bins, mean_bins=True) p_j /= p_j.sum() # find (maximum, minimum) of the binned distribution h_max, h_min = np.max(p_j), np.min(p_j) # compute the PAC pac[a, p, tr] = (h_max - h_min) / h_max return pac
[docs]def phase_locking_value_nb(pha, pha_amp): """Numba-based Phase Locking-Value (PLV). In order to measure the phase locking value, the phase of the amplitude of the higher-frequency signal must be provided, and not the amplitude as in most other PAC functions. Parameters ---------- pha, pha_amp : array_like Respectively the arrays of phases of shape (n_pha, n_epochs, n_times) for the lower frequency and the array of phase of the amplitude signal of shape (n_pha_amp, n_epochs, n_times) for the higher frequency. Both arrays should be of type float64 (np.float64) Returns ------- pac : array_like Array of phase amplitude coupling of shape (n_pha_amp, n_pha, ...) References ---------- Penny et al. 2008 :cite:`penny2008testing`, Lachaux et al. 1999 :cite:`lachaux1999measuring` """ n_pha, n_epochs, n_times = pha.shape n_amp, _, _ = pha_amp.shape pac = np.zeros((n_amp, n_pha, n_epochs), dtype=np.float64) # single conversion exp_pha = np.exp(1j * pha) exp_pha_amp = np.exp(-1j * pha_amp) for a in range(n_amp): for p in range(n_pha): for tr in range(n_epochs): _pha = exp_pha[p, tr, :] _pha_amp = exp_pha_amp[a, tr, :] pac[a, p, tr] = abs(np.dot(_pha, _pha_amp)) pac /= n_times return pac
""" I don't think this function can be entirely compiled with Numba because of two issues : * Numba supports the mean / std but not across a specific axis * erfinv is a special function of scipy that don't seems to be supported for the moment Therefore, the beginning and the end of the function are tensor-based while the core function that computes PAC is the Numba compliant MVL. """
[docs]def norm_direct_pac_nb(pha, amp, p=.05): """Numba-based Normalized direct Pac (ndPAC). Parameters ---------- pha, amp : array_like Respectively the arrays of phases of shape (n_pha, n_epochs, n_times) and the array of amplitudes of shape (n_amp, n_epochs, n_times). Both arrays should be of type float64 (np.float64) p : float | .05 P-value to use for thresholding. Sub-threshold PAC values will be set to 0. To disable this behavior (no masking), use ``p=1`` or ``p=None``. Should be a np.float64 Returns ------- pac : array_like Array of phase amplitude coupling of shape (n_amp, n_pha, ...) References ---------- Ozkurt et al. :cite:`ozkurt2012statistically` """ n_times = pha.shape[-1] # z-score normalization to approximate assumptions amp = np.subtract(amp, np.mean(amp, axis=-1, keepdims=True)) amp = np.divide(amp, np.std(amp, ddof=1, axis=-1, keepdims=True)) # compute pac using MVL (need to remultiply by n_times) pac = mean_vector_length_nb(pha, amp) * n_times # no thresholding if p == 1. or p is None: return pac / n_times s = pac ** 2 pac /= n_times # set to zero non-significant values xlim = n_times * erfinv(1 - p) ** 2 pac[s <= 2 * xlim] = 0. return pac