Repository: mne-tools/mne-python

Issue:
Concatenation of epochs raises non-intuitive error if one epoch is empty
#### Describe the bug

When concatenating a list of epochs, when one of the epochs contains zero events, a ValueError with few information is raised.

#### 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_1 = np.random.random((n_epochs, n_channels, n_timepoints))
fake_data_2 = np.random.random((n_epochs, n_channels, n_timepoints))

info = mne.create_info(ch_names=ch_names, sfreq=40)
epo_1 = mne.EpochsArray(fake_data_1, info)
epo_2 = mne.EpochsArray(fake_data_2, info)

# works
conc_epos = mne.concatenate_epochs([epo_1, epo_2])
# remove all data from epo_2
epo_2.drop(range(10))
# Get cryptic ValueError: 
# zero-size array to reduction operation maximum which has no identity
not_conc_epos = mne.concatenate_epochs([epo_1, epo_2])
```

#### Expected results

Either get a concatenated epoch object that contains all events from `epo_1` with a warning that some epos were empty, or get a ValueError that better explains what's happening.


#### Actual results

MNE raises the following error:

```
Traceback (most recent call last):
  File "<input>", line 4, in <module>
  File "/home/jan/dev/python/spot/spot_venv/lib/python3.8/site-packages/mne/epochs.py", line 3493, in concatenate_epochs
    return _finish_concat(*_concatenate_epochs(epochs_list,
  File "/home/jan/dev/python/spot/spot_venv/lib/python3.8/site-packages/mne/epochs.py", line 3419, in _concatenate_epochs
    events_offset = _update_offset(events_offset, epochs.events, shift)
  File "/home/jan/dev/python/spot/spot_venv/lib/python3.8/site-packages/mne/epochs.py", line 3360, in _update_offset
    offset = np.int64(offset) + np.max(events[:, 0]) + shift
  File "<__array_function__ internals>", line 5, in amax
  File "/home/jan/dev/python/spot/spot_venv/lib/python3.8/site-packages/numpy/core/fromnumeric.py", line 2705, in amax
    return _wrapreduction(a, np.maximum, 'max', axis, None, out,
  File "/home/jan/dev/python/spot/spot_venv/lib/python3.8/site-packages/numpy/core/fromnumeric.py", line 87, in _wrapreduction
    return ufunc.reduce(obj, axis, dtype, out, **passkwargs)
ValueError: zero-size array to reduction operation maximum which has no identity
```

My current workaround is to manually filter out empty epochs beforehand.

#### Additional information

Platform:      Linux-5.8.0-59-generic-x86_64-with-glibc2.29
Python:        3.8.10 (default, Jun  2 2021, 10:49:15)  [GCC 9.4.0]
Executable:    /home/jan/dev/python/spot/spot_venv/bin/python
CPU:           x86_64: 8 cores
Memory:        Unavailable (requires "psutil" package)
mne:           0.23.0
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




Discussion:

Resolving patch:
diff --git a/mne/epochs.py b/mne/epochs.py
--- a/mne/epochs.py
+++ b/mne/epochs.py
@@ -3304,18 +3304,6 @@ def add_channels_epochs(epochs_list, verbose=None):
     return epochs
 
 
-def _update_offset(offset, events, shift):
-    if offset == 0:
-        return offset
-    offset = 0 if offset is None else offset
-    offset = np.int64(offset) + np.max(events[:, 0]) + shift
-    if offset > INT32_MAX:
-        warn(f'Event number greater than {INT32_MAX} created, events[:, 0] '
-             'will be assigned consecutive increasing integer values')
-        offset = 0
-    return offset
-
-
 def _concatenate_epochs(epochs_list, with_data=True, add_offset=True, *,
                         on_mismatch='raise'):
     """Auxiliary function for concatenating epochs."""
@@ -3341,7 +3329,8 @@ def _concatenate_epochs(epochs_list, with_data=True, add_offset=True, *,
     selection = out.selection
     # offset is the last epoch + tmax + 10 second
     shift = int((10 + tmax) * out.info['sfreq'])
-    events_offset = _update_offset(None, out.events, shift)
+    events_offset = 0
+    events_number_overflow = False
     for ii, epochs in enumerate(epochs_list[1:], 1):
         _ensure_infos_match(epochs.info, info, f'epochs[{ii}]',
                             on_mismatch=on_mismatch)
@@ -3365,11 +3354,19 @@ def _concatenate_epochs(epochs_list, with_data=True, add_offset=True, *,
             epochs.drop_bad()
             offsets.append(len(epochs))
         evs = epochs.events.copy()
-        # add offset
-        if add_offset:
-            evs[:, 0] += events_offset
-        # Update offset for the next iteration.
-        events_offset = _update_offset(events_offset, epochs.events, shift)
+        if len(epochs.events) == 0:
+            warn('One of the Epochs objects to concatenate was empty.')
+        elif add_offset:
+            # We need to cast to a native Python int here to prevent an
+            # overflow of a numpy int32 or int64 type.
+            events_offset += int(np.max(evs[:, 0])) + shift
+            if events_offset > INT32_MAX:
+                warn(f'Event number greater than {INT32_MAX} created, '
+                     'events[:, 0] will be assigned consecutive increasing '
+                     'integer values')
+                events_number_overflow = True
+            else:
+                evs[:, 0] += events_offset
         events.append(evs)
         selection = np.concatenate((selection, epochs.selection))
         drop_log = drop_log + epochs.drop_log
@@ -3377,7 +3374,7 @@ def _concatenate_epochs(epochs_list, with_data=True, add_offset=True, *,
         metadata.append(epochs.metadata)
     events = np.concatenate(events, axis=0)
     # check to see if we exceeded our maximum event offset
-    if events_offset == 0:
+    if events_number_overflow:
         events[:, 0] = np.arange(1, len(events) + 1)
 
     # Create metadata object (or make it None)


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
@@ -2700,6 +2700,11 @@ def test_concatenate_epochs():
     with pytest.raises(ValueError, match='identical keys'):
         concatenate_epochs([epochs1, epochs2])
 
+    # check concatenating epochs where one of the objects is empty
+    epochs2 = epochs.copy()[:0]
+    with pytest.warns(RuntimeWarning, match='was empty'):
+        concatenate_epochs([epochs, epochs2])
+
 
 def test_concatenate_epochs_large():
     """Test concatenating epochs on large data."""