Repository: mne-tools/mne-python

Issue:
FIX: Fix multitaper computation
@dengemann thanks for the tip about `nitime`. This should work for both old and new `nitime` versions. @mluessi I think this was some of your code, you okay with the naming?

Ready for review and merge, hopefully quickly so that `master` can be fixed. Europeans/east-coasters, feel free to take over if it expedites the merge (e.g., while I'm asleep).



Discussion:

Resolving patch:
diff --git a/mne/decoding/classifier.py b/mne/decoding/classifier.py
--- a/mne/decoding/classifier.py
+++ b/mne/decoding/classifier.py
@@ -190,11 +190,16 @@ class PSDEstimator(TransformerMixin):
         bandwidth.
     n_jobs : int
         Number of parallel jobs to use (only used if adaptive=True).
+    normalization : str
+        Either "full" or "length" (default). If "full", the PSD will
+        be normalized by the sampling rate as well as the length of
+        the signal (as in nitime).
     verbose : bool, str, int, or None
         If not None, override default verbose level (see mne.verbose).
     """
     def __init__(self, sfreq=2 * np.pi, fmin=0, fmax=np.inf, bandwidth=None,
-                 adaptive=False, low_bias=True, n_jobs=1, verbose=None):
+                 adaptive=False, low_bias=True, n_jobs=1, normalization='length',
+                 verbose=None):
         self.sfreq = sfreq
         self.fmin = fmin
         self.fmax = fmax
@@ -203,6 +208,7 @@ def __init__(self, sfreq=2 * np.pi, fmin=0, fmax=np.inf, bandwidth=None,
         self.low_bias = low_bias
         self.n_jobs = n_jobs
         self.verbose = verbose
+        self.normalization = normalization
 
     def fit(self, epochs_data, y):
         """Compute power spectrum density (PSD) using a multi-taper method
@@ -249,9 +255,12 @@ def transform(self, epochs_data, y=None):
         n_epochs, n_channels, n_times = epochs_data.shape
         X = epochs_data.reshape(n_epochs * n_channels, n_times)
 
-        psd, _ = multitaper_psd(X, self.sfreq, self.fmin, self.fmax,
-                                self.bandwidth, self.adaptive, self.low_bias,
-                                self.n_jobs, self.verbose)
+        psd, _ = multitaper_psd(x=X, sfreq=self.sfreq, fmin=self.fmin,
+                                fmax=self.fmax, bandwidth=self.bandwidth,
+                                adaptive=self.adaptive, low_bias=self.low_bias,
+                                n_jobs=self.n_jobs,
+                                normalization=self.normalization,
+                                verbose=self.verbose)
 
         _, n_freqs = psd.shape
         psd = psd.reshape(n_epochs, n_channels, n_freqs)
diff --git a/mne/time_frequency/multitaper.py b/mne/time_frequency/multitaper.py
--- a/mne/time_frequency/multitaper.py
+++ b/mne/time_frequency/multitaper.py
@@ -452,7 +452,8 @@ def _mt_spectra(x, dpss, sfreq, n_fft=None):
 
 @verbose
 def multitaper_psd(x, sfreq=2 * np.pi, fmin=0, fmax=np.inf, bandwidth=None,
-                   adaptive=False, low_bias=True, n_jobs=1, verbose=None):
+                   adaptive=False, low_bias=True, n_jobs=1,
+                   normalization='length', verbose=None):
     """Compute power spectrum density (PSD) using a multi-taper method
 
     Parameters
@@ -475,6 +476,10 @@ def multitaper_psd(x, sfreq=2 * np.pi, fmin=0, fmax=np.inf, bandwidth=None,
         bandwidth.
     n_jobs : int
         Number of parallel jobs to use (only used if adaptive=True).
+    normalization : str
+        Either "full" or "length" (default). If "full", the PSD will
+        be normalized by the sampling rate as well as the length of
+        the signal (as in nitime).
     verbose : bool, str, int, or None
         If not None, override default verbose level (see mne.verbose).
 
@@ -485,6 +490,9 @@ def multitaper_psd(x, sfreq=2 * np.pi, fmin=0, fmax=np.inf, bandwidth=None,
     freqs : array
         The frequency points in Hz of the PSD.
     """
+    if normalization not in ('length', 'full'):
+        raise ValueError('Normalization must be "length" or "full", not %s'
+                         % normalization)
     if x.ndim > 2:
         raise ValueError('x can only be 1d or 2d')
 
@@ -531,5 +539,7 @@ def multitaper_psd(x, sfreq=2 * np.pi, fmin=0, fmax=np.inf, bandwidth=None,
         psd = psd[0, :]
 
     freqs = freqs[freq_mask]
+    if normalization == 'full':
+        psd /= sfreq
 
     return psd, freqs


Test patch:
diff --git a/mne/time_frequency/tests/test_multitaper.py b/mne/time_frequency/tests/test_multitaper.py
--- a/mne/time_frequency/tests/test_multitaper.py
+++ b/mne/time_frequency/tests/test_multitaper.py
@@ -1,5 +1,7 @@
 import numpy as np
+from nose.tools import assert_raises
 from numpy.testing import assert_array_almost_equal
+from distutils.version import LooseVersion
 
 from mne.time_frequency import dpss_windows, multitaper_psd
 from mne.utils import requires_nitime
@@ -20,8 +22,10 @@ def test_dpss_windows():
     assert_array_almost_equal(dpss, dpss_ni)
     assert_array_almost_equal(eigs, eigs_ni)
 
-    dpss, eigs = dpss_windows(N, half_nbw, Kmax, interp_from=200, low_bias=False)
-    dpss_ni, eigs_ni = ni.algorithms.dpss_windows(N, half_nbw, Kmax, interp_from=200)
+    dpss, eigs = dpss_windows(N, half_nbw, Kmax, interp_from=200,
+                              low_bias=False)
+    dpss_ni, eigs_ni = ni.algorithms.dpss_windows(N, half_nbw, Kmax,
+                                                  interp_from=200)
 
     assert_array_almost_equal(dpss, dpss_ni)
     assert_array_almost_equal(eigs, eigs_ni)
@@ -35,11 +39,15 @@ def test_multitaper_psd():
     n_times = 1000
     x = np.random.randn(5, n_times)
     sfreq = 500
+    assert_raises(ValueError, multitaper_psd, x, sfreq, normalization='foo')
+    ni_5 = (LooseVersion(ni.__version__) >= LooseVersion('0.5'))
+    norm = 'full' if ni_5 else 'length'
 
     for adaptive, n_jobs in zip((False, True, True), (1, 1, 2)):
-        psd, freqs = multitaper_psd(x, sfreq, adaptive=adaptive, n_jobs=n_jobs)
+        psd, freqs = multitaper_psd(x, sfreq, adaptive=adaptive, n_jobs=n_jobs,
+                                    normalization=norm)
         freqs_ni, psd_ni, _ = ni.algorithms.spectral.multi_taper_psd(x, sfreq,
-                              adaptive=adaptive, jackknife=False)
+            adaptive=adaptive, jackknife=False)
 
         # for some reason nitime returns n_times + 1 frequency points
         # causing the value at 0 to be different