Repository: mne-tools/mne-python

Issue:
connectivity.spectral_connectivity drops times when given Epochs object 
#### Describe the bug
I'm trying to compute EEG functional connectivity in various time intervals relative to the stimulus.   
I run into a problem with the `tmin` and `tmax` arguments in `mne.connectivity.spectral_connectivity`.  

If the data object passed to `mne.connectivity.spectral_connectivity` is of type `mne.Epochs`, then `spectral._prepare_connectivity` should use the times from `mne.Epochs.times`:
Otherwise, `times` is generated using `np.linspace()` from the sampling frequency `sfreq` and the number of data samples (`n_times_in`).

This is done in `_prepare_connectivity` with a subset of the data, which itself tries to create the `times_in` through `_get_and_verify_data_sizes`.

Here, I want to get from -0.4 to -0.1 seconds from the event (300 ms span 100 ms before event.)
Here is an evoked plot with the full time axis with the marked interval in which I want to compute connectivity. 

![image](https://user-images.githubusercontent.com/44297909/106517400-b8bc6500-64d8-11eb-8557-864c3f8c9d87.png)

However, the time points from `epochs.times` are lost in the execution of `spectral._prepare_connectivity` and replaced with generated time axis. This makes the time points provided in `tmin` and `tmax` no longer consistent with the time in the data object. 

A possible fix to this issue is to pass `times_in` to both `spectral._prepare_connectivity` and `spectral._get_and_verify_data_sizes`, with which I then get my expected results. Something like:
Definition of  [`_prepare_connectivity`](https://github.com/mne-tools/mne-python/blob/4f2925b19ed8dc54a237021de2912732348ae65c/mne/connectivity/spectral.py#L933) :
```python
def _prepare_connectivity(epoch_block, tmin, tmax, fmin, fmax, sfreq, indices,
                          mode, fskip, n_bands,
                          cwt_freqs, faverage, times_in=None):
    """Check and precompute dimensions of results data."""
    first_epoch = epoch_block[0]

    # get the data size and time scale
    n_signals, n_times_in, times_in = _get_and_verify_data_sizes(first_epoch, times=times_in)
...
```
Calling [`_prepare_connectivity`](https://github.com/mne-tools/mne-python/blob/4f2925b19ed8dc54a237021de2912732348ae65c/mne/connectivity/spectral.py#L785 ):
```python
...
(n_cons, times, n_times, times_in, n_times_in, tmin_idx,
             tmax_idx, n_freqs, freq_mask, freqs, freqs_bands, freq_idx_bands,
             n_signals, indices_use) = _prepare_connectivity(
                epoch_block=epoch_block, tmin=tmin, tmax=tmax, fmin=fmin,
                fmax=fmax, sfreq=sfreq, indices=indices, mode=mode,
                fskip=fskip, n_bands=n_bands,
                cwt_freqs=cwt_freqs, faverage=faverage, times_in=times_in)
...
```

#### Steps to reproduce
```python
import mne
from mne import io
from mne.connectivity import spectral_connectivity
from mne.datasets import sample
from mne.viz import plot_connectivity_circle

data_path = sample.data_path()
raw_fname = data_path + '/MEG/sample/sample_audvis_filt-0-40_raw.fif'
event_fname = data_path + '/MEG/sample/sample_audvis_filt-0-40_raw-eve.fif'

raw = io.read_raw_fif(raw_fname)
events = mne.read_events(event_fname)
raw.info['bads'] += ['MEG 2443']
picks = mne.pick_types(raw.info, meg='grad', eeg=False, stim=False, eog=True,
                       exclude='bads')

event_id, tmin, tmax = 3, -1, 1.5
epochs = mne.Epochs(raw, events, event_id, tmin, tmax, picks=picks,
                    baseline=(None, 0), reject=dict(grad=4000e-13, eog=150e-6))

fmin, fmax = 10., 12.
sfreq = raw.info['sfreq']


tmin = -0.5
tmax = -0.1
epochs.load_data().pick_types(meg='grad')
con, freqs, times, n_epochs, n_tapers = spectral_connectivity(
    epochs, method='pli', mode='multitaper', sfreq=sfreq, fmin=fmin, fmax=fmax,
    faverage=True, tmin=tmin, tmax=tmax, mt_adaptive=False, n_jobs=1)

plot_connectivity_circle(con[:,:,0], epochs.ch_names, n_lines=300)

```


#### Expected results
I would expect the `times` from the `epochs` object to be used, rather than time points generated from 0. 

#### Actual results
```python
Connectivity computation...
Traceback (most recent call last):
  File "/home/ap/PycharmProjects/eeg_processing/conn_example.py", line 30, in <module>
    faverage=True, tmin=tmin, tmax=tmax, mt_adaptive=False, n_jobs=1)
  File "<decorator-gen-387>", line 21, in spectral_connectivity
  File "/home/ap/miniconda3/envs/MNE/lib/python3.7/site-packages/mne/connectivity/spectral.py", line 789, in spectral_connectivity
    cwt_freqs=cwt_freqs, faverage=faverage)
  File "/home/ap/miniconda3/envs/MNE/lib/python3.7/site-packages/mne/connectivity/spectral.py", line 948, in _prepare_connectivity
    mask = _time_mask(times_in, tmin, tmax, sfreq=sfreq)
  File "/home/ap/miniconda3/envs/MNE/lib/python3.7/site-packages/mne/utils/numerics.py", line 499, in _time_mask
    % (orig_tmin, orig_tmax, extra, times[0], times[-1]))
ValueError: No samples remain when using tmin=-0.5 and tmax=-0.1 (original time bounds are [0.0, 2.4974401644798485])
```
With a bit of guidance, I'm happy to make PR to resolve the issue :)

#### Additional information
```
mne.sys_info()
Backend Qt5Agg is interactive backend. Turning interactive mode on.
Platform:      Linux-5.0.0-32-generic-x86_64-with-debian-buster-sid
Python:        3.7.9 (default, Aug 31 2020, 12:42:55)  [GCC 7.3.0]
Executable:    /home/ap/miniconda3/envs/MNE2/bin/python
CPU:           x86_64: 12 cores
Memory:        Unavailable (requires "psutil" package)
mne:           0.22.0
numpy:         1.19.2 {blas=mkl_rt, lapack=mkl_rt}
scipy:         1.5.2
matplotlib:    3.3.2 {backend=Qt5Agg}
sklearn:       0.23.2
numba:         Not found
nibabel:       Not found
nilearn:       Not found
dipy:          Not found
cupy:          Not found
pandas:        1.2.1
mayavi:        Not found
pyvista:       Not found
vtk:           Not found
```


Discussion:
I have not looked at the code but what you describe sounds like a bug. I would start by coding some simple test that fails on `master` -- for example a two-epoch `EpochsArray` where -0.5 to 0 has some signal that is coherent and 0 to 0.5 does not -- and show that it fails there. Then when you write your code you already have the test in place that should pass once things are fixed. A PR for this would be great!

Resolving patch:
diff --git a/mne/connectivity/spectral.py b/mne/connectivity/spectral.py
--- a/mne/connectivity/spectral.py
+++ b/mne/connectivity/spectral.py
@@ -18,7 +18,7 @@
                                          _psd_from_mt, _csd_from_mt,
                                          _psd_from_mt_adaptive)
 from ..time_frequency.tfr import morlet, cwt
-from ..utils import logger, verbose, _time_mask, warn
+from ..utils import logger, verbose, _time_mask, warn, _arange_div
 
 ########################################################################
 # Various connectivity estimators
@@ -475,7 +475,8 @@ def _check_method(method):
     return True, None
 
 
-def _get_and_verify_data_sizes(data, n_signals=None, n_times=None, times=None):
+def _get_and_verify_data_sizes(data, sfreq, n_signals=None, n_times=None,
+                               times=None):
     """Get and/or verify the data sizes and time scales."""
     if not isinstance(data, (list, tuple)):
         raise ValueError('data has to be a list or tuple')
@@ -491,12 +492,15 @@ def _get_and_verify_data_sizes(data, n_signals=None, n_times=None, times=None):
         n_signals_tot += this_n_signals
 
         if hasattr(this_data, 'times'):
+            assert isinstance(this_data, _BaseSourceEstimate)
             this_times = this_data.times
             if times is not None:
                 if np.any(times != this_times):
                     warn('time scales of input time series do not match')
             else:
                 times = this_times
+        elif times is None:
+            times = _arange_div(n_times, sfreq)
 
     if n_signals is not None:
         if n_signals != n_signals_tot:
@@ -772,6 +776,8 @@ def spectral_connectivity(data, method='coh', indices=None, sfreq=2 * np.pi,
     if isinstance(data, BaseEpochs):
         times_in = data.times  # input times for Epochs input type
         sfreq = data.info['sfreq']
+    else:
+        times_in = None
 
     # loop over data; it could be a generator that returns
     # (n_signals x n_times) arrays or SourceEstimates
@@ -783,9 +789,9 @@ def spectral_connectivity(data, method='coh', indices=None, sfreq=2 * np.pi,
             (n_cons, times, n_times, times_in, n_times_in, tmin_idx,
              tmax_idx, n_freqs, freq_mask, freqs, freqs_bands, freq_idx_bands,
              n_signals, indices_use) = _prepare_connectivity(
-                epoch_block=epoch_block, tmin=tmin, tmax=tmax, fmin=fmin,
-                fmax=fmax, sfreq=sfreq, indices=indices, mode=mode,
-                fskip=fskip, n_bands=n_bands,
+                epoch_block=epoch_block, times_in=times_in,
+                tmin=tmin, tmax=tmax, fmin=fmin, fmax=fmax, sfreq=sfreq,
+                indices=indices, mode=mode, fskip=fskip, n_bands=n_bands,
                 cwt_freqs=cwt_freqs, faverage=faverage)
 
             # get the window function, wavelets, etc for different modes
@@ -823,8 +829,8 @@ def spectral_connectivity(data, method='coh', indices=None, sfreq=2 * np.pi,
 
         # check dimensions and time scale
         for this_epoch in epoch_block:
-            _get_and_verify_data_sizes(this_epoch, n_signals, n_times_in,
-                                       times_in)
+            _get_and_verify_data_sizes(
+                this_epoch, sfreq, n_signals, n_times_in, times_in)
 
         call_params = dict(
             sig_idx=sig_idx, tmin_idx=tmin_idx,
@@ -930,7 +936,8 @@ def spectral_connectivity(data, method='coh', indices=None, sfreq=2 * np.pi,
     return con, freqs, times, n_epochs, n_tapers
 
 
-def _prepare_connectivity(epoch_block, tmin, tmax, fmin, fmax, sfreq, indices,
+def _prepare_connectivity(epoch_block, times_in, tmin, tmax,
+                          fmin, fmax, sfreq, indices,
                           mode, fskip, n_bands,
                           cwt_freqs, faverage):
     """Check and precompute dimensions of results data."""
@@ -938,14 +945,18 @@ def _prepare_connectivity(epoch_block, tmin, tmax, fmin, fmax, sfreq, indices,
     first_epoch = epoch_block[0]
 
     # get the data size and time scale
-    n_signals, n_times_in, times_in = _get_and_verify_data_sizes(first_epoch)
-
-    if times_in is None:
-        # we are not using Epochs or SourceEstimate(s) as input
-        times_in = np.linspace(0.0, n_times_in / sfreq, n_times_in,
-                               endpoint=False)
+    n_signals, n_times_in, times_in = _get_and_verify_data_sizes(
+        first_epoch, sfreq, times=times_in)
 
     n_times_in = len(times_in)
+
+    if tmin is not None and tmin < times_in[0]:
+        warn('start time tmin=%0.2f s outside of the time scope of the data '
+             '[%0.2f s, %0.2f s]' % (tmin, times_in[0], times_in[-1]))
+    if tmax is not None and tmax > times_in[-1]:
+        warn('stop time tmax=%0.2f s outside of the time scope of the data '
+             '[%0.2f s, %0.2f s]' % (tmax, times_in[0], times_in[-1]))
+
     mask = _time_mask(times_in, tmin, tmax, sfreq=sfreq)
     tmin_idx, tmax_idx = np.where(mask)[0][[0, -1]]
     tmax_idx += 1


Test patch:
diff --git a/mne/connectivity/tests/test_spectral.py b/mne/connectivity/tests/test_spectral.py
--- a/mne/connectivity/tests/test_spectral.py
+++ b/mne/connectivity/tests/test_spectral.py
@@ -2,11 +2,9 @@
 from numpy.testing import assert_array_almost_equal
 import pytest
 
+from mne import EpochsArray, SourceEstimate, create_info
 from mne.connectivity import spectral_connectivity
 from mne.connectivity.spectral import _CohEst, _get_n_epochs
-
-from mne import SourceEstimate
-from mne.utils import run_tests_if_main
 from mne.filter import filter_data
 
 
@@ -212,4 +210,48 @@ def test_spectral_connectivity(method, mode):
     assert (out_lens[0] == 10)
 
 
-run_tests_if_main()
+@pytest.mark.parametrize('kind', ('epochs', 'ndarray', 'stc'))
+def test_epochs_tmin_tmax(kind):
+    """Test spectral.spectral_connectivity with epochs and arrays."""
+    rng = np.random.RandomState(0)
+    n_epochs, n_chs, n_times, sfreq, f = 10, 2, 2000, 1000., 20.
+    data = rng.randn(n_epochs, n_chs, n_times)
+    sig = np.sin(2 * np.pi * f * np.arange(1000) / sfreq) * np.hanning(1000)
+    data[:, :, 500:1500] += sig
+    info = create_info(n_chs, sfreq, 'eeg')
+    if kind == 'epochs':
+        tmin = -1
+        X = EpochsArray(data, info, tmin=tmin)
+    elif kind == 'stc':
+        tmin = -1
+        X = [SourceEstimate(d, [[0], [0]], tmin, 1. / sfreq) for d in data]
+    else:
+        assert kind == 'ndarray'
+        tmin = 0
+        X = data
+
+    # Parameters for computing connectivity
+    fmin, fmax = f - 2, f + 2
+    kwargs = {'method': 'coh', 'mode': 'multitaper', 'sfreq': sfreq,
+              'fmin': fmin, 'fmax': fmax, 'faverage': True,
+              'mt_adaptive': False, 'n_jobs': 1}
+
+    # Check the entire interval
+    conn = spectral_connectivity(X, **kwargs)
+    assert 0.89 < conn[0][1, 0] < 0.91
+    # Check a time interval before the sinusoid
+    conn = spectral_connectivity(X, tmax=tmin + 0.5, **kwargs)
+    assert 0 < conn[0][1, 0] < 0.15
+    # Check a time during the sinusoid
+    conn = spectral_connectivity(X, tmin=tmin + 0.5, tmax=tmin + 1.5, **kwargs)
+    assert 0.93 < conn[0][1, 0] <= 0.94
+    # Check a time interval after the sinusoid
+    conn = spectral_connectivity(X, tmin=tmin + 1.5, tmax=tmin + 1.9, **kwargs)
+    assert 0 < conn[0][1, 0] < 0.15
+
+    # Check for warning if tmin, tmax is outside of the time limits of data
+    with pytest.warns(RuntimeWarning, match='start time tmin'):
+        spectral_connectivity(X, **kwargs, tmin=tmin - 0.1)
+
+    with pytest.warns(RuntimeWarning, match='stop time tmax'):
+        spectral_connectivity(X, **kwargs, tmax=tmin + 2.5)