initial commit
This commit is contained in:
167
mne/preprocessing/ctps_.py
Normal file
167
mne/preprocessing/ctps_.py
Normal file
@@ -0,0 +1,167 @@
|
||||
# Authors: The MNE-Python contributors.
|
||||
# License: BSD-3-Clause
|
||||
# Copyright the MNE-Python contributors.
|
||||
|
||||
import math
|
||||
|
||||
import numpy as np
|
||||
from scipy.signal import hilbert
|
||||
from scipy.special import logsumexp
|
||||
|
||||
|
||||
def _compute_normalized_phase(data):
|
||||
"""Compute normalized phase angles.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
data : ndarray, shape (n_epochs, n_sources, n_times)
|
||||
The data to compute the phase angles for.
|
||||
|
||||
Returns
|
||||
-------
|
||||
phase_angles : ndarray, shape (n_epochs, n_sources, n_times)
|
||||
The normalized phase angles.
|
||||
"""
|
||||
return (np.angle(hilbert(data)) + np.pi) / (2 * np.pi)
|
||||
|
||||
|
||||
def ctps(data, is_raw=True):
|
||||
"""Compute cross-trial-phase-statistics [1].
|
||||
|
||||
Note. It is assumed that the sources are already
|
||||
appropriately filtered
|
||||
|
||||
Parameters
|
||||
----------
|
||||
data: ndarray, shape (n_epochs, n_channels, n_times)
|
||||
Any kind of data of dimensions trials, traces, features.
|
||||
is_raw : bool
|
||||
If True it is assumed that data haven't been transformed to Hilbert
|
||||
space and phase angles haven't been normalized. Defaults to True.
|
||||
|
||||
Returns
|
||||
-------
|
||||
ks_dynamics : ndarray, shape (n_sources, n_times)
|
||||
The kuiper statistics.
|
||||
pk_dynamics : ndarray, shape (n_sources, n_times)
|
||||
The normalized kuiper index for ICA sources and
|
||||
time slices.
|
||||
phase_angles : ndarray, shape (n_epochs, n_sources, n_times) | None
|
||||
The phase values for epochs, sources and time slices. If ``is_raw``
|
||||
is False, None is returned.
|
||||
|
||||
References
|
||||
----------
|
||||
[1] Dammers, J., Schiek, M., Boers, F., Silex, C., Zvyagintsev,
|
||||
M., Pietrzyk, U., Mathiak, K., 2008. Integration of amplitude
|
||||
and phase statistics for complete artifact removal in independent
|
||||
components of neuromagnetic recordings. Biomedical
|
||||
Engineering, IEEE Transactions on 55 (10), 2353-2362.
|
||||
"""
|
||||
if not data.ndim == 3:
|
||||
raise ValueError(f"Data must have 3 dimensions, not {data.ndim}.")
|
||||
|
||||
if is_raw:
|
||||
phase_angles = _compute_normalized_phase(data)
|
||||
else:
|
||||
phase_angles = data # phase angles can be computed externally
|
||||
|
||||
# initialize array for results
|
||||
ks_dynamics = np.zeros_like(phase_angles[0])
|
||||
pk_dynamics = np.zeros_like(phase_angles[0])
|
||||
|
||||
# calculate Kuiper's statistic for each source
|
||||
for ii, source in enumerate(np.transpose(phase_angles, [1, 0, 2])):
|
||||
ks, pk = kuiper(source)
|
||||
pk_dynamics[ii, :] = pk
|
||||
ks_dynamics[ii, :] = ks
|
||||
|
||||
return ks_dynamics, pk_dynamics, phase_angles if is_raw else None
|
||||
|
||||
|
||||
def kuiper(data, dtype=np.float64): # noqa: D401
|
||||
"""Kuiper's test of uniform distribution.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
data : ndarray, shape (n_sources,) | (n_sources, n_times)
|
||||
Empirical distribution.
|
||||
dtype : str | obj
|
||||
The data type to be used.
|
||||
|
||||
Returns
|
||||
-------
|
||||
ks : ndarray
|
||||
Kuiper's statistic.
|
||||
pk : ndarray
|
||||
Normalized probability of Kuiper's statistic [0, 1].
|
||||
"""
|
||||
# if data not numpy array, implicitly convert and make to use copied data
|
||||
# ! sort data array along first axis !
|
||||
data = np.sort(data, axis=0).astype(dtype)
|
||||
shape = data.shape
|
||||
n_dim = len(shape)
|
||||
n_trials = shape[0]
|
||||
|
||||
# create uniform cdf
|
||||
j1 = (np.arange(n_trials, dtype=dtype) + 1.0) / float(n_trials)
|
||||
j2 = np.arange(n_trials, dtype=dtype) / float(n_trials)
|
||||
if n_dim > 1: # single phase vector (n_trials)
|
||||
j1 = j1[:, np.newaxis]
|
||||
j2 = j2[:, np.newaxis]
|
||||
d1 = (j1 - data).max(axis=0)
|
||||
d2 = (data - j2).max(axis=0)
|
||||
n_eff = n_trials
|
||||
|
||||
d = d1 + d2 # Kuiper's statistic [n_time_slices]
|
||||
|
||||
return d, _prob_kuiper(d, n_eff, dtype=dtype)
|
||||
|
||||
|
||||
def _prob_kuiper(d, n_eff, dtype="f8"):
|
||||
"""Test for statistical significance against uniform distribution.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
d : float
|
||||
The kuiper distance value.
|
||||
n_eff : int
|
||||
The effective number of elements.
|
||||
dtype : str | obj
|
||||
The data type to be used. Defaults to double precision floats.
|
||||
|
||||
Returns
|
||||
-------
|
||||
pk_norm : float
|
||||
The normalized Kuiper value such that 0 < ``pk_norm`` < 1.
|
||||
|
||||
References
|
||||
----------
|
||||
[1] Stephens MA 1970. Journal of the Royal Statistical Society, ser. B,
|
||||
vol 32, pp 115-122.
|
||||
|
||||
[2] Kuiper NH 1962. Proceedings of the Koninklijke Nederlands Akademie
|
||||
van Wetenschappen, ser Vol 63 pp 38-47
|
||||
"""
|
||||
n_time_slices = np.size(d) # single value or vector
|
||||
n_points = 100
|
||||
|
||||
en = math.sqrt(n_eff)
|
||||
k_lambda = (en + 0.155 + 0.24 / en) * d # see [1]
|
||||
l2 = k_lambda**2.0
|
||||
j2 = (np.arange(n_points) + 1) ** 2
|
||||
j2 = j2.repeat(n_time_slices).reshape(n_points, n_time_slices)
|
||||
fact = 4.0 * j2 * l2 - 1.0
|
||||
|
||||
# compute normalized pK value in range [0,1]
|
||||
a = -2.0 * j2 * l2
|
||||
b = 2.0 * fact
|
||||
pk_norm = -logsumexp(a, b=b, axis=0) / (2.0 * n_eff)
|
||||
|
||||
# check for no difference to uniform cdf
|
||||
pk_norm = np.where(k_lambda < 0.4, 0.0, pk_norm)
|
||||
|
||||
# check for round off errors
|
||||
pk_norm = np.where(pk_norm > 1.0, 1.0, pk_norm)
|
||||
|
||||
return pk_norm
|
||||
Reference in New Issue
Block a user