Repository: mne-tools/mne-python

Issue:
n_fft issue
Changing n_fft does not change the frequency bins when using
`mne.time_frequency.psd_welch`



Discussion:
is `n_fft > n_samples` for your signal?

yes

Read the docstring of `n_fft` more carefully, it says that if `n_fft > n_samples` , then `n_samples` will be used, i.e. `n_fft = min(n_fft, n_samples)`. Make sense?

Yes I see that sorry about that. I had previously had n_fft  > n_samples and I got the zero-padded result. Not qute sure what function I used. Does mne support zero-padding or should I have to zero-pad the data before turning it into an Epoch?

I know that tfr_\* does that but what if I needed padding without the need to do time-frequency analysis? In this case where I fix the time bounds computation time is faster.

IIRC psd_welch does not support padding and I also remember myself expecting at the beginning that `n_fft > n_samples` does zero padding. 
The `scipy.signal.welch` function that `psd_welch` is using supports zero-padding however: `nperseg` argument controls segment length and `nfft` is used for zero-padding, when needed.

I suppose we could change it so that if `n_fft > n_samples` it zero-pads, seems more consistent. Not sure how to do it in a nice way from a non-API-breaking standpoint, though. We might need to introduce a kwarg :(

In some cases one may still want a welch window that is shorter than n_samples and zero-padding too, so another kwarg seems necessary (`pad_to`, `pad`, `zero_pad` ?).

I agree

@mmagnuski did you change this behavior in #3823? If so can you update `whats_new.rst` to say so, and we can close this?
No, I have a local branch for this. I will try to push it tomorrow.
Great thanks for fixing these issues

Resolving patch:
diff --git a/mne/time_frequency/psd.py b/mne/time_frequency/psd.py
--- a/mne/time_frequency/psd.py
+++ b/mne/time_frequency/psd.py
@@ -11,17 +11,26 @@
 from .multitaper import psd_array_multitaper
 
 
-def _psd_func(epoch, noverlap, nfft, fs, freq_mask, func):
+def _psd_func(epoch, noverlap, n_per_seg, nfft, fs, freq_mask, func):
     """Aux function."""
-    return func(epoch, fs=fs, nperseg=nfft, noverlap=noverlap,
+    return func(epoch, fs=fs, nperseg=n_per_seg, noverlap=noverlap,
                 nfft=nfft, window='hann')[2][..., freq_mask, :]
 
 
-def _check_nfft(n, n_fft, n_overlap):
-    """Helper to make sure n_fft and n_overlap make sense."""
-    n_fft = n if n_fft > n else n_fft
-    n_overlap = n_fft - 1 if n_overlap >= n_fft else n_overlap
-    return n_fft, n_overlap
+def _check_nfft(n, n_fft, n_per_seg, n_overlap):
+    """Helper to make sure n_fft, n_per_seg and n_overlap make sense."""
+    if n_per_seg is None and n_fft > n:
+        raise ValueError(('If n_per_seg is None n_fft is not allowed to be > '
+                          'n_times. If you want zero-padding, you have to set '
+                          'n_per_seg to relevant length. Got n_fft of %d while'
+                          ' signal length is %d.') % (n_fft, n))
+    n_per_seg = n_fft if n_per_seg is None or n_per_seg > n_fft else n_per_seg
+    n_per_seg = n if n_per_seg > n else n_per_seg
+    if n_overlap >= n_per_seg:
+        raise ValueError(('n_overlap cannot be greater than n_per_seg (or '
+                          'n_fft). Got n_overlap of %d while n_per_seg is '
+                          '%d.') % (n_overlap, n_per_seg))
+    return n_fft, n_per_seg, n_overlap
 
 
 def _check_psd_data(inst, tmin, tmax, picks, proj):
@@ -54,7 +63,7 @@ def _check_psd_data(inst, tmin, tmax, picks, proj):
 
 @verbose
 def psd_array_welch(x, sfreq, fmin=0, fmax=np.inf, n_fft=256, n_overlap=0,
-                    n_jobs=1, verbose=None):
+                    n_per_seg=None, n_jobs=1, verbose=None):
     """Compute power spectral density (PSD) using Welch's method.
 
     Parameters
@@ -68,13 +77,15 @@ def psd_array_welch(x, sfreq, fmin=0, fmax=np.inf, n_fft=256, n_overlap=0,
     fmax : float
         The upper frequency of interest.
     n_fft : int
-        The length of the tapers ie. the windows. The smaller
-        it is the smoother are the PSDs. The default value is 256.
-        If ``n_fft > len(inst.times)``, it will be adjusted down to
-        ``len(inst.times)``.
+        The length of FFT used, must be ``>= n_per_seg`` (default: 256).
+        The segments will be zero-padded if ``n_fft > n_per_seg``.
     n_overlap : int
-        The number of points of overlap between blocks. Will be adjusted
-        to be <= n_fft. The default value is 0.
+        The number of points of overlap between segments. Will be adjusted
+        to be <= n_per_seg. The default value is 0.
+    n_per_seg : int | None
+        Length of each Welch segment. The smaller it is with respect to the
+        signal length the smoother are the PSDs. Defaults to None, which sets
+        n_per_seg equal to n_fft.
     n_jobs : int
         Number of CPUs to use in the computation.
     verbose : bool, str, int, or None
@@ -99,7 +110,8 @@ def psd_array_welch(x, sfreq, fmin=0, fmax=np.inf, n_fft=256, n_overlap=0,
     x = x.reshape(-1, n_times)
 
     # Prep the PSD
-    n_fft, n_overlap = _check_nfft(n_times, n_fft, n_overlap)
+    n_fft, n_per_seg, n_overlap = _check_nfft(n_times, n_fft, n_per_seg,
+                                              n_overlap)
     win_size = n_fft / float(sfreq)
     logger.info("Effective window size : %0.3f (s)" % win_size)
     freqs = np.arange(n_fft // 2 + 1, dtype=float) * (sfreq / n_fft)
@@ -111,7 +123,8 @@ def psd_array_welch(x, sfreq, fmin=0, fmax=np.inf, n_fft=256, n_overlap=0,
     x_splits = np.array_split(x, n_jobs)
     f_spectrogram = parallel(my_psd_func(d, noverlap=n_overlap, nfft=n_fft,
                                          fs=sfreq, freq_mask=freq_mask,
-                                         func=spectrogram) for d in x_splits)
+                                         func=spectrogram, n_per_seg=n_per_seg)
+                             for d in x_splits)
 
     # Combining, reducing windows and reshaping to original data shape
     # XXX : we can certainly avoid the allocation before the mean
@@ -122,7 +135,8 @@ def psd_array_welch(x, sfreq, fmin=0, fmax=np.inf, n_fft=256, n_overlap=0,
 
 @verbose
 def psd_welch(inst, fmin=0, fmax=np.inf, tmin=None, tmax=None, n_fft=256,
-              n_overlap=0, picks=None, proj=False, n_jobs=1, verbose=None):
+              n_overlap=0, n_per_seg=None, picks=None, proj=False, n_jobs=1,
+              verbose=None):
     """Compute the power spectral density (PSD) using Welch's method.
 
     Calculates periodigrams for a sliding window over the
@@ -141,13 +155,15 @@ def psd_welch(inst, fmin=0, fmax=np.inf, tmin=None, tmax=None, n_fft=256,
     tmax : float | None
         Max time of interest
     n_fft : int
-        The length of the tapers ie. the windows. The smaller
-        it is the smoother are the PSDs. The default value is 256.
-        If ``n_fft > len(inst.times)``, it will be adjusted down to
-        ``len(inst.times)``.
+        The length of FFT used, must be ``>= n_per_seg`` (default: 256).
+        The segments will be zero-padded if ``n_fft > n_per_seg``.
     n_overlap : int
-        The number of points of overlap between blocks. Will be adjusted
-        to be <= n_fft. The default value is 0.
+        The number of points of overlap between segments. Will be adjusted
+        to be <= n_per_seg. The default value is 0.
+    n_per_seg : int | None
+        Length of each Welch segment. The smaller it is with respect to the
+        signal length the smoother are the PSDs. Defaults to None, which sets
+        n_per_seg equal to n_fft.
     picks : array-like of int | None
         The selection of channels to include in the computation.
         If None, take all channels.
@@ -180,7 +196,8 @@ def psd_welch(inst, fmin=0, fmax=np.inf, tmin=None, tmax=None, n_fft=256,
     # Prep data
     data, sfreq = _check_psd_data(inst, tmin, tmax, picks, proj)
     return psd_array_welch(data, sfreq, fmin=fmin, fmax=fmax, n_fft=n_fft,
-                           n_overlap=n_overlap, n_jobs=n_jobs, verbose=verbose)
+                           n_overlap=n_overlap, n_per_seg=n_per_seg,
+                           n_jobs=n_jobs, verbose=verbose)
 
 
 @verbose


Test patch:
diff --git a/mne/io/array/tests/test_array.py b/mne/io/array/tests/test_array.py
--- a/mne/io/array/tests/test_array.py
+++ b/mne/io/array/tests/test_array.py
@@ -92,7 +92,7 @@ def test_array_raw():
 
     # plotting
     raw2.plot()
-    raw2.plot_psd(tmax=np.inf, average=True, spatial_colors=False)
+    raw2.plot_psd(tmax=np.inf, average=True, n_fft=1024, spatial_colors=False)
     plt.close('all')
 
     # epoching
diff --git a/mne/time_frequency/tests/test_psd.py b/mne/time_frequency/tests/test_psd.py
--- a/mne/time_frequency/tests/test_psd.py
+++ b/mne/time_frequency/tests/test_psd.py
@@ -62,6 +62,22 @@ def test_psd():
         # Array input shouldn't work
         assert_raises(ValueError, func, raw[:3, :20][0])
 
+    # test n_per_seg in psd_welch (and padding)
+    psds1, freqs1 = psd_welch(raw, proj=False, n_fft=128, n_per_seg=128,
+                              **kws_psd)
+    psds2, freqs2 = psd_welch(raw, proj=False, n_fft=256, n_per_seg=128,
+                              **kws_psd)
+    assert_true(len(freqs1) == np.floor(len(freqs2) / 2.))
+    assert_true(psds1.shape[-1] == np.floor(psds2.shape[-1] / 2.))
+
+    # tests ValueError when n_per_seg=None and n_fft > signal length
+    kws_psd.update(dict(n_fft=tmax * 1.1 * raw.info['sfreq']))
+    assert_raises(ValueError, psd_welch, raw, proj=False, n_per_seg=None,
+                  **kws_psd)
+    # ValueError when n_overlap > n_per_seg
+    kws_psd.update(dict(n_fft=128, n_per_seg=64, n_overlap=90))
+    assert_raises(ValueError, psd_welch, raw, proj=False, **kws_psd)
+
     # -- Epochs/Evoked --
     events = read_events(event_fname)
     events[:, 0] -= first_samp