Repository: mne-tools/mne-python

Issue:
`mne.Epochs` sends wrong log messages (1) on metdata, (2) when `decim` is passed
`mne.Epochs` sends wrong log messages (1) on metdata, (2) when `decim` is passed.

MWE:

Try first running this code:

```Python
import os
import mne

sample_data_folder = mne.datasets.sample.data_path()
sample_data_raw_file = os.path.join(sample_data_folder, 'MEG', 'sample',
                                    'sample_audvis_raw.fif')
raw = mne.io.read_raw_fif(sample_data_raw_file, verbose=False).crop(tmax=120)
raw.load_data()
raw.filter(h_freq=40, l_freq=None)

events = mne.find_events(raw, stim_channel='STI 014')
event_dict = {'auditory/left': 1, 'auditory/right': 2, 'visual/left': 3,
              'visual/right': 4, 'face': 5, 'button': 32}
epochs = mne.Epochs(raw, events, tmin=-0.2, tmax=0.5, event_id=event_dict,
                    decim=1, preload=True)
print(epochs.times.shape)

```

This is the logged output, where I can see that the `Not setting metadata` log is unnecessarily repeated.
On the last lines of the log, we see that 421 time points were loaded, and this is indeed the shape of `.times`

```text
176 events found
Event IDs: [ 1  2  3  4  5 32]
Not setting metadata
Not setting metadata
176 matching events found
Setting baseline interval to [-0.19979521315838786, 0.0] sec
Applying baseline correction (mode: mean)
Created an SSP operator (subspace dimension = 3)
3 projection items activated
Loading data for 176 events and 421 original time points ...
1 bad epochs dropped
(421,)
```

Now change `decim=1` in the `mne.Epochs()` call above to `decim=2`. I would expect the log 

> Loading data for 176 events and 421 original time points ...

to change in terms of "original time points" loaded (should be half as many)

However, this is what we get:


This is the logged output, where the shape of `.times` has indeed halved as expected, yet the log tells us that 421 time points were loaded.

```text
176 events found
Event IDs: [ 1  2  3  4  5 32]
Not setting metadata
Not setting metadata
176 matching events found
Setting baseline interval to [-0.19979521315838786, 0.0] sec
Applying baseline correction (mode: mean)
Created an SSP operator (subspace dimension = 3)
3 projection items activated
Loading data for 176 events and 421 original time points ...
1 bad epochs dropped
(211,)

```

# Needed actions

1. remove duplicate log message on setting metadata. Don't really know where to fix, maybe around here: https://github.com/mne-tools/mne-python/blob/a3066f6eb3730d13d2dce632ff6cb5d4870fbe7f/mne/utils/mixin.py#L412-L418


2. adjust logging about loaded time points when creating epochs and decimating. Probably to be fixed somewhere around here: https://github.com/mne-tools/mne-python/blob/a3066f6eb3730d13d2dce632ff6cb5d4870fbe7f/mne/epochs.py#L1388-L1389


Discussion:
> to change in terms of "original time points" loaded (should be half as many)

IIRC "original" here I think is meant to mean "pre-decimated" / "raw". Maybe we should just say "raw" since it does load that many time points and then they are decimated...?
I see :thinking: that makes sense. Does Epochs also "load" these data if the `raw` I pass is already pre-loaded?

> Maybe we should just say "raw" since it does load that many time points and then they are decimated...?

or - if `decimate>1` - modify the log to say:

> Loading data for 176 events and 421 original time points **(prior to decimation)** ...
> Does Epochs also "load" these data if the raw I pass is already pre-loaded?

Not sure actually

> or - if decimate>1 - modify the log to say:

+1 for me, would make things less clearer
> +1 for me, would make things less clearer

less unclear?

> Not sure actually

Me neither, I thought preloading Raw would speed up making the Epochs, but maybe/probably not (**EDIT: see below**):

Calling Epochs does an init of BaseEpochs, where `data=None` 

https://github.com/mne-tools/mne-python/blob/c3ff05c05c7bfcf765249f27a517803f27f48b4e/mne/epochs.py#L2585-L2591

https://github.com/mne-tools/mne-python/blob/c3ff05c05c7bfcf765249f27a517803f27f48b4e/mne/epochs.py#L342-L344

And if it's None, it's read from Raw using the `load_data` method.

**EDIT** _However, the load_data method does make use of preloaded Raw ... so interestingly, Epochs will log that it's loading data, even if it is not (in the case where Raw passed to Epochs was preloaded)_

---

Then what remains open in this issue are the duplicate logs of setting metadata. This is becomes more confusing when one actually **passes** metadata, because in the first log it says "setting metadata ... " and then in the next log it says "replacing metadata ... "

It's called here: https://github.com/mne-tools/mne-python/blob/c3ff05c05c7bfcf765249f27a517803f27f48b4e/mne/epochs.py#L411

and then a couple of lines below: https://github.com/mne-tools/mne-python/blob/c3ff05c05c7bfcf765249f27a517803f27f48b4e/mne/epochs.py#L445

I believe the first call can be deleted, WDYT?

Resolving patch:
diff --git a/mne/epochs.py b/mne/epochs.py
--- a/mne/epochs.py
+++ b/mne/epochs.py
@@ -409,7 +409,10 @@ def __init__(self, info, data, events, event_id=None, tmin=-0.2, tmax=0.5,
             # ensure metadata matches original events size
             self.selection = np.arange(len(events))
             self.events = events
-            self.metadata = metadata
+
+            # same as self.metadata = metadata, but suppress log in favor
+            # of logging below (after setting self.selection)
+            GetEpochsMixin.metadata.fset(self, metadata, verbose=False)
             del events
 
             values = list(self.event_id.values())
@@ -443,6 +446,11 @@ def __init__(self, info, data, events, event_id=None, tmin=-0.2, tmax=0.5,
                 metadata = [metadata[s] for s in sub]
             elif metadata is not None:
                 metadata = metadata.iloc[sub]
+
+            # Remove temporarily set metadata from above, and set
+            # again to get the correct log ("adding metadata", instead of
+            # "replacing existing metadata")
+            GetEpochsMixin.metadata.fset(self, None, verbose=False)
             self.metadata = metadata
             del metadata
 
@@ -1385,8 +1393,14 @@ def _get_data(self, out=True, picks=None, item=None, *, units=None,
         else:
             # we start out with an empty array, allocate only if necessary
             data = np.empty((0, len(self.info['ch_names']), len(self.times)))
-            logger.info('Loading data for %s events and %s original time '
-                        'points ...' % (n_events, len(self._raw_times)))
+            msg = (f'for {n_events} events and {len(self._raw_times)} '
+                   'original time points')
+            if self._decim > 1:
+                msg += ' (prior to decimation)'
+            if getattr(self._raw, "preload", False):
+                logger.info(f'Using data from preloaded Raw {msg} ...')
+            else:
+                logger.info(f'Loading data {msg} ...')
 
         orig_picks = picks
         if orig_picks is 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
@@ -38,7 +38,7 @@
     bootstrap, equalize_epoch_counts, combine_event_ids, add_channels_epochs,
     EpochsArray, concatenate_epochs, BaseEpochs, average_movements,
     _handle_event_repeated, make_metadata)
-from mne.utils import (requires_pandas, object_diff,
+from mne.utils import (requires_pandas, object_diff, use_log_level,
                        catch_logging, _FakeNoPandas,
                        assert_meg_snr, check_version, _dt_to_stamp)
 
@@ -1023,13 +1023,23 @@ def test_epochs_io_preload(tmp_path, preload):
     # for some tests
     tols = dict(atol=1e-3, rtol=1e-20)
 
-    raw, events, picks = _get_data(preload=True)
+    raw, events, picks = _get_data(preload=preload)
     tempdir = str(tmp_path)
     temp_fname = op.join(tempdir, 'test-epo.fif')
     temp_fname_no_bl = op.join(tempdir, 'test_no_bl-epo.fif')
     baseline = (None, 0)
-    epochs = Epochs(raw, events, event_id, tmin, tmax, picks=picks,
-                    baseline=baseline, preload=True)
+    with catch_logging() as log:
+        epochs = Epochs(raw, events, event_id, tmin, tmax, picks=picks,
+                        baseline=baseline, preload=True, verbose=True)
+    log = log.getvalue()
+    msg = 'Not setting metadata'
+    assert log.count(msg) == 1, f'\nto find:\n{msg}\n\nlog:\n{log}'
+    load_msg = 'Loading data for 7 events and 421 original time points ...'
+    if preload:
+        load_msg = ('Using data from preloaded Raw for 7 events and 421 '
+                    'original time points ...')
+    assert log.count(load_msg) == 1, f'\nto find:\n{load_msg}\n\nlog:\n{log}'
+
     evoked = epochs.average()
     epochs.save(temp_fname, overwrite=True)
 
@@ -1397,8 +1407,13 @@ def test_evoked_io_from_epochs(tmp_path):
     with raw.info._unlock():
         raw.info['lowpass'] = 40  # avoid aliasing warnings
     # offset our tmin so we don't get exactly a zero value when decimating
-    epochs = Epochs(raw, events[:4], event_id, tmin + 0.011, tmax,
-                    picks=picks, decim=5)
+    with catch_logging() as log:
+        epochs = Epochs(raw, events[:4], event_id, tmin + 0.011, tmax,
+                        picks=picks, decim=5, preload=True, verbose=True)
+    log = log.getvalue()
+    load_msg = ('Loading data for 1 events and 415 original time points '
+                '(prior to decimation) ...')
+    assert log.count(load_msg) == 1, f'\nto find:\n{load_msg}\n\nlog:\n{log}'
     evoked = epochs.average()
     with evoked.info._unlock():
         # Test that empty string shortcuts to None.
@@ -2926,8 +2941,17 @@ def test_metadata(tmp_path):
     events = np.column_stack([events, np.zeros([len(events), 2])]).astype(int)
     events[5:, -1] = 1
     event_id = {'zero': 0, 'one': 1}
-    epochs = EpochsArray(data, info, metadata=meta,
-                         events=events, event_id=event_id)
+    with catch_logging() as log:
+        epochs = EpochsArray(data, info, metadata=meta,
+                             events=events, event_id=event_id, verbose=True)
+    log = log.getvalue()
+    msg = 'Adding metadata with 2 columns'
+    assert log.count(msg) == 1, f'\nto find:\n{msg}\n\nlog:\n{log}'
+    with use_log_level(True):
+        with catch_logging() as log:
+            epochs.metadata = meta
+    log = log.getvalue().strip()
+    assert log == 'Replacing existing metadata with 2 columns', f'{log}'
     indices = np.arange(len(epochs))  # expected indices
     assert_array_equal(epochs.metadata.index, indices)