Repository: scipy/scipy
Pull request: BUG: Set nperseg size to the size of an already-initialized window on periodogram
<!-- 
Thanks for contributing a pull request! Please ensure that
your PR satisfies the checklist before submitting:
http://scipy.github.io/devdocs/dev/contributor/development_workflow.html#checklist-before-submitting-a-pr

Also, please name and describe your PR as you would write a
commit message:
http://scipy.github.io/devdocs/dev/contributor/development_workflow.html#writing-the-commit-message

Note that we are a team of volunteers; we appreciate your
patience during the review process.

Again, thanks for contributing!
-->

#### Reference issue
Fixes #14809

#### What does this implement/fix?
<!--Please explain your changes.-->
This PR makes sure that any window (array-like) that is passed as parameter to `periodogram` that has a length less than the input signal is used as-it-is instead of raising error. By doing so, the periodogram analysis is truncated to the size of the window

#### Additional information
<!--Any additional information you think is important.-->


Problem statement:
BUG: scipy.signal.periodogram window parameter
### Describe your issue.

When trying to pass an array as the `window` for scipy's periodogram, the behavior is inconsistent with the documentation. In the doc it says:
*window : str or tuple or array_like, optional
    Desired window to use. If `window` is a string or tuple, it is
    passed to `get_window` to generate the window values, which are
    DFT-even by default. See `get_window` for a list of windows and
    required parameters. If `window` is array_like it will be used
    directly as the window and its length **must be nperseg**. Defaults
    to 'boxcar'.*
However there is no **nperseg** parameter for `scipy.signal.periodogram`! Moreover, a confusing error message is thrown, as shown below.
I guess it's intended use is for windows precisely the size of the signal `x`. However in a signal analysis set up, it can be meaningful to first convolve the signal with a (shorter) window before performing the FFT (think moving average, although it's often a bad idea). When using the `nfft` parameter shorter windows work but I assume the signal gets truncated?

### Reproducing Code Example

```python
import numpy as np
from scipy.signal import windows, periodogram
                                                                                                                          
nSamples = 1000
win_size = nSamples // 4
signal = np.random.randn(nSamples)                                                                                        
                                                                                                                          
# manual short hann window as array                                                                                             
hann = windows.hann(win_size)                                                                                             

freqs, pxx = periodogram(signal, window=hann)

# this works, however doesn't fit the documentation
# not clear what exactly is happening here.. does it truncate the input signal?
# freqs, pxx = periodogram(signal, window=hann, nfft=win_size)
```


### Error message

```shell
~/.local/lib/python3.8/site-packages/scipy/signal/spectral.py in _triage_segments(window, nperseg, input_length)
   1977         elif nperseg is not None:
   1978             if nperseg != win.shape[0]:
-> 1979                 raise ValueError("value specified for nperseg is different"
   1980                                  " from length of window")
   1981     return win, nperseg

ValueError: value specified for nperseg is different from length of window
```


### SciPy/NumPy/Python version information

1.4.1 1.18.4 sys.version_info(major=3, minor=8, micro=10, releaselevel='final', serial=0)

Hints from issue discussion:
Thanks for the bug report @tensionhead 

> I guess it's intended use is for windows precisely the size of the signal `x`. 

Yes, I believe that is correct. This is also what ends up happening under the hood when you give a string name for the window (e.g., `'hann'`). In `periodogram`, `nperseg` is set to the length of the axis and then in `_triage_segments`:
```python
win = get_window(window, nperseg)
```
There `win` is an array with length `nperseg`.

The docstring for `periodogram` says _" If `window` is array_like it will be used directly as the window and its length must be nperseg."_ Here `nperseg` should be replaced by _"equal to the length of the axis over which the periodogram is computed"_.

> However in a signal analysis set up, it can be meaningful to first convolve the signal with a (shorter) window before performing the FFT (think moving average, although it's often a bad idea). 

The current way to achieve this would be to use `ndimage.convolve1d` I think.

> When using the `nfft` parameter shorter windows work but I assume the signal gets truncated?

Only works if you set `nfft` to the window size you are using. I'd say that that does a different (worse) thing than `ndimage.convolve1d`, because it tries to use an FFT-based convolution while what I think you want is a regular convolution - then any short window size works fine.

To resolve this issue (as @andfoy is doing in gh-16776), I think we need:
1. An updated parameter description for `window`, as I suggested above,
2. A clearer error message that explains that if `window` is an array, what its length must be given the shape of input `x`,
3. A regression test checking the error raised is the correct one.

Commit message: FIX: Compare the size of the window w.r.t the input

Raises an exception if necessary

Patch to review:
diff --git a/scipy/signal/_spectral_py.py b/scipy/signal/_spectral_py.py
index d5bfe01c82c4..ec9b4da4855a 100644
--- a/scipy/signal/_spectral_py.py
+++ b/scipy/signal/_spectral_py.py
@@ -153,8 +153,9 @@ def lombscargle(x,
     return pgram
 
 
-def periodogram(x, fs=1.0, window='boxcar', nfft=None, detrend='constant',
-                return_onesided=True, scaling='density', axis=-1):
+def periodogram(x, fs=1.0, window='boxcar', nfft=None,
+                detrend='constant', return_onesided=True,
+                scaling='density', axis=-1):
     """
     Estimate power spectral density using a periodogram.
 
@@ -169,7 +170,8 @@ def periodogram(x, fs=1.0, window='boxcar', nfft=None, detrend='constant',
         passed to `get_window` to generate the window values, which are
         DFT-even by default. See `get_window` for a list of windows and
         required parameters. If `window` is array_like it will be used
-        directly as the window and its length must be nperseg. Defaults
+        directly as the window and its length must be equal to the length
+        of the axis over which the periodogram is computed. Defaults
         to 'boxcar'.
     nfft : int, optional
         Length of the FFT used. If `None` the length of `x` will be
@@ -267,6 +269,12 @@ def periodogram(x, fs=1.0, window='boxcar', nfft=None, detrend='constant',
 
     if window is None:
         window = 'boxcar'
+    elif hasattr(window, 'size'):
+        axis_size = x.shape[axis]
+        window_size = window.size
+        if window_size != axis_size:
+            raise ValueError('the size of the window must be the same size '
+                             'of the input on the specified axis')
 
     if nfft is None:
         nperseg = x.shape[axis]


Code review comments:
diff_hunk: @@ -267,6 +269,12 @@ def periodogram(x, fs=1.0, window='boxcar', nfft=None, detrend='constant',
 
     if window is None:
         window = 'boxcar'
+    elif hasattr(window, 'size'):
+        axis_size = x.shape[axis]
+        window_size = window.size
+        if window_size != axis_size:
path: scipy/signal/_spectral_py.py
text: @user1:
Almost right, it looks like this check must happen after `nperseg` is determined (because it isn't always equal to `x.shape[axis]` and the correct criterion is if window_size != nperseg`. The error message does look fine.

diff_hunk: @@ -153,8 +153,9 @@ def lombscargle(x,
     return pgram
 
 
-def periodogram(x, fs=1.0, window='boxcar', nfft=None, detrend='constant',
-                return_onesided=True, scaling='density', axis=-1):
+def periodogram(x, fs=1.0, window='boxcar', nfft=None,
+                detrend='constant', return_onesided=True,
+                scaling='density', axis=-1):
path: scipy/signal/_spectral_py.py
text: @user1:
There's no change to this anymore, so can you undo the formatting changes?