Repository: mne-tools/mne-python

Issue:
Repeated Epoch/Evoked cropping is not idempotent when include_tmax=False
#### Describe the bug

Repeated cropping of `mne.Epoch` when `include_tmax=False` cuts off the last time sample.

#### Steps to reproduce

```python
import mne
import numpy as np

n_epochs, n_channels, n_timepoints = 10, 4, 21
ch_names = [f"ch_{i}" for i in range(n_channels)]
fake_data = np.random.random((n_epochs, n_channels, n_timepoints))

info = mne.create_info(ch_names=ch_names, sfreq=40)
epo = mne.EpochsArray(fake_data, info)

for i in range(10):
    epo.crop(tmin=0, tmax=0.4, include_tmax=False)
    print(f"{epo.tmax=}")
```

#### Expected results

The `epo.tmax` should be set to 0.375 on the first and all subsequent iterations.

#### Actual results

Every loop iteration cuts off the last time sample of the epoch.

#### Additional information

```
Platform:      Linux-5.4.0-72-generic-x86_64-with-glibc2.29
Python:        3.8.5 (default, Jan 27 2021, 15:41:15)  [GCC 9.3.0]
Executable:    /home/jan/dev/python/spot/spot_venv/bin/python
CPU:           x86_64: 8 cores
Memory:        Unavailable (requires "psutil" package)
mne:           0.24.dev0
numpy:         1.19.5 {blas=openblas, lapack=openblas}
scipy:         1.5.4
matplotlib:    3.3.4 {backend=module://backend_interagg}

sklearn:       0.23.2
numba:         Not found
nibabel:       3.2.1
nilearn:       Not found
dipy:          Not found
cupy:          Not found
pandas:        1.1.5
mayavi:        Not found
pyvista:       Not found
vtk:           Not found
```


The cause of the issue is that when the `tmax` argument for `crop` is out of range of the `Epoch`, it is set to `epo.tmax`. However, this behaviour is (at least for me) unintuitive when `include_tmax=False` as I do not want to cut off the last time sample, if my desired interval is larger than the epoch.

Possible solution would be to set `tmax` to `tmax + 1/sfreq` when `include_tmax=True` and the desired tmax-crop is outside the epoch interval.



Discussion:
the pb is here:

https://github.com/mne-tools/mne-python/blob/main/mne/utils/numerics.py#L475

we need remove the last time point even if we shouldn't

would you be willing to open a PR to fix this?

>

Sure, I will submit a PR.

Resolving patch:
diff --git a/mne/epochs.py b/mne/epochs.py
--- a/mne/epochs.py
+++ b/mne/epochs.py
@@ -1638,6 +1638,7 @@ def crop(self, tmin=None, tmax=None, include_tmax=True, verbose=None):
             warn('tmax is not in epochs time interval. tmax is set to '
                  'epochs.tmax')
             tmax = self.tmax
+            include_tmax = True
 
         tmask = _time_mask(self.times, tmin, tmax, sfreq=self.info['sfreq'],
                            include_tmax=include_tmax)
diff --git a/mne/evoked.py b/mne/evoked.py
--- a/mne/evoked.py
+++ b/mne/evoked.py
@@ -334,6 +334,7 @@ def crop(self, tmin=None, tmax=None, include_tmax=True, verbose=None):
             warn(f'tmax is not in Evoked time interval. tmax is set to '
                  f'evoked.tmax ({self.tmax:g} sec)')
             tmax = self.tmax
+            include_tmax = True
 
         mask = _time_mask(self.times, tmin, tmax, sfreq=self.info['sfreq'],
                           include_tmax=include_tmax)


Test patch:
diff --git a/mne/tests/test_epochs.py b/mne/tests/test_epochs.py
--- a/mne/tests/test_epochs.py
+++ b/mne/tests/test_epochs.py
@@ -1621,6 +1621,14 @@ def test_crop(tmpdir):
     assert np.isclose(epochs_cropped.tmax, epochs_cropped.reject_tmax)
     del epochs_cropped
 
+    # Test that repeated cropping is idempotent
+    epoch_crop = epochs.copy()
+    epoch_crop.crop(None, 0.4, include_tmax=False)
+    n_times = len(epoch_crop.times)
+    with pytest.warns(RuntimeWarning, match='tmax is set to'):
+        epoch_crop.crop(None, 0.4, include_tmax=False)
+        assert len(epoch_crop.times) == n_times
+
     # Cropping & I/O roundtrip
     epochs.crop(0, 0.1)
     epochs.save(temp_fname)
diff --git a/mne/tests/test_evoked.py b/mne/tests/test_evoked.py
--- a/mne/tests/test_evoked.py
+++ b/mne/tests/test_evoked.py
@@ -680,6 +680,10 @@ def test_time_as_index_and_crop():
     assert_array_equal(evoked.time_as_index([-.1, .1], use_rounding=True),
                        [0, len(evoked.times) - 1])
     evoked.crop(evoked.tmin, evoked.tmax, include_tmax=False)
+    n_times = len(evoked.times)
+    with pytest.warns(RuntimeWarning, match='tmax is set to'):
+        evoked.crop(tmin, tmax, include_tmax=False)
+    assert len(evoked.times) == n_times
     assert_allclose(evoked.times[[0, -1]], [tmin, tmax - delta], atol=atol)