Repository: mne-tools/mne-python

Issue:
memory leak when creating epochs
Together with @wmvanvliet we [observed a memory leak](https://github.com/mne-tools/mne-biomag-group-demo/issues/6) when creating epochs. Here is a script to reproduce:

```py
import os.path as op
import mne

from memory_profiler import memory_usage

data_path = mne.datasets.sample.data_path()
fname = op.join(data_path, 'MEG', 'sample', 'sample_audvis_raw.fif')


def mne_size(inst):
    repr_ = inst.__repr__().split(', ')
    return float([r[1:-3] for r in repr_ if 'MB' in r][0])


def run():
    epochs = list()
    for i in range(15):
        raw = mne.io.read_raw_fif(fname, preload=True)

        events = mne.find_events(raw)

        tmin, tmax = -0.2, 0.5
        event_id = {'Auditory/Left': 1, 'Auditory/Right': 2,
                    'Visual/Left': 3, 'Visual/Right': 4}
        picks = mne.pick_types(raw.info, meg=True, eeg=False, eog=True)
        epochs.append(mne.Epochs(raw, events=events, event_id=event_id,
                      tmin=tmin, tmax=tmax, reject=None,
                      picks=picks, preload=True))

    total_size = mne_size(epochs[0]) * len(epochs) + mne_size(raw)
    print('Size reported by MNE: %0.2f MB' % total_size)


real_size = memory_usage(run, max_usage=True)
print('Actual size in memory: %0.2f MB' % real_size[0])
```
Please change the number of iterations 15 before trying in your laptop if it can't handle more than 10 GB.


Discussion:
why do you call this a memory leak? it is by design that Epochs have a ref on raw and if raw is preloaded then you have it in memory. Use memmap for raw to avoid this.
Why does the Epochs need to have a ref to raw once it is preloaded? I don't need the raw object anymore as the epochs have been created and loaded into the disk
certain functions like tf_lcmv need it but maybe this can be questioned
indeed.

What is the benefit of keeping track of the original `raw` when `preload=True`? When loading an `epochs` there is no `.raw` attribute and everything seems to work ok.
then it means that functions that need Epochs.raw as tf_lcmv should require
preload=False

what if you change the reject param sequentially as @jasmainak implemented
some time ago?

ah ok, so it is used? Then we should leave it as it is and change the biomag demo instead.
I think this should be eventually fixed in MNE though and the lcmv examples changed. 

It is counterintuitive, since the `__repr__` claims that we consume less memory. I seem to remember that we used to delete the reference to `raw` after loading the data in Epochs. But perhaps I am mistaken.

I don't see a problem with sequential reject param as long as the threshold gets stricter (which was enforced).
ok so maybe we can fix this....

Yeah it seems a bit silly to keep it around after preload. I'll take a look

Resolving patch:
diff --git a/mne/beamformer/_lcmv.py b/mne/beamformer/_lcmv.py
--- a/mne/beamformer/_lcmv.py
+++ b/mne/beamformer/_lcmv.py
@@ -945,7 +945,7 @@ def _lcmv_source_power(info, forward, noise_cov, data_cov, reg=0.05,
 def tf_lcmv(epochs, forward, noise_covs, tmin, tmax, tstep, win_lengths,
             freq_bins, subtract_evoked=False, reg=0.05, label=None,
             pick_ori=None, n_jobs=1, picks=None, rank=None,
-            weight_norm='unit-noise-gain', verbose=None):
+            weight_norm='unit-noise-gain', raw=None, verbose=None):
     """5D time-frequency beamforming based on LCMV.
 
     Calculate source power in time-frequency windows using a spatial filter
@@ -960,7 +960,11 @@ def tf_lcmv(epochs, forward, noise_covs, tmin, tmax, tstep, win_lengths,
     Parameters
     ----------
     epochs : Epochs
-        Single trial epochs.
+        Single trial epochs. It is recommended to pass epochs that have
+        been constructed with ``preload=False`` (i.e., not preloaded or
+        read from disk) so that the parameter ``raw=None`` can be used
+        below, as this ensures the correct :class:`mne.io.Raw` instance is
+        used for band-pass filtering.
     forward : dict
         Forward operator.
     noise_covs : list of instances of Covariance | None
@@ -1003,6 +1007,10 @@ def tf_lcmv(epochs, forward, noise_covs, tmin, tmax, tstep, win_lengths,
     weight_norm: 'unit-noise-gain'| None
         If 'unit-noise-gain', the unit-noise gain minimum variance beamformer
         will be computed (Borgiotti-Kaplan beamformer) [2]_.
+    raw : instance of Raw | None
+        The raw instance used to construct the epochs.
+        Must be provided unless epochs are constructed with
+        ``preload=False``.
     verbose : bool, str, int, or None
         If not None, override default verbose level (see :func:`mne.verbose`
         and :ref:`Logging documentation <tut_logging>` for more).
@@ -1036,11 +1044,12 @@ def tf_lcmv(epochs, forward, noise_covs, tmin, tmax, tstep, win_lengths,
                          'window lengths')
 
     # Extract raw object from the epochs object
-    raw = epochs._raw
+    raw = epochs._raw if raw is None else raw
     if raw is None:
         raise ValueError('The provided epochs object does not contain the '
                          'underlying raw object. Please use preload=False '
-                         'when constructing the epochs object.')
+                         'when constructing the epochs object or pass the '
+                         'underlying raw instance to this function')
 
     if noise_covs is None:
         picks = _setup_picks(picks, epochs.info, forward)
diff --git a/mne/epochs.py b/mne/epochs.py
--- a/mne/epochs.py
+++ b/mne/epochs.py
@@ -203,9 +203,9 @@ class BaseEpochs(ProjMixin, ContainsMixin, UpdateChannelsMixin,
 
     Notes
     -----
-    The `BaseEpochs` class is public to allow for stable type-checking in user
-    code (i.e., ``isinstance(my_epochs, BaseEpochs)``) but should not be used
-    as a constructor for Epochs objects (use instead `mne.Epochs`).
+    The ``BaseEpochs`` class is public to allow for stable type-checking in
+    user code (i.e., ``isinstance(my_epochs, BaseEpochs)``) but should not be
+    used as a constructor for Epochs objects (use instead :class:`mne.Epochs`).
     """
 
     def __init__(self, info, data, events, event_id=None, tmin=-0.2, tmax=0.5,
@@ -394,6 +394,7 @@ def load_data(self):
         self._decim = 1
         self._raw_times = self.times
         assert self._data.shape[-1] == len(self.times)
+        self._raw = None  # shouldn't need it anymore
         return self
 
     @verbose


Test patch:
diff --git a/mne/beamformer/tests/test_lcmv.py b/mne/beamformer/tests/test_lcmv.py
--- a/mne/beamformer/tests/test_lcmv.py
+++ b/mne/beamformer/tests/test_lcmv.py
@@ -350,7 +350,7 @@ def test_tf_lcmv():
     epochs = mne.Epochs(raw, events, event_id, tmin, tmax, proj=True,
                         baseline=None, preload=False,
                         reject=dict(grad=4000e-13, mag=4e-12, eog=150e-6))
-    epochs.drop_bad()
+    epochs.load_data()
 
     freq_bins = [(4, 12), (15, 40)]
     time_windows = [(-0.1, 0.1), (0.0, 0.2)]
@@ -390,9 +390,10 @@ def test_tf_lcmv():
                         reg=reg, label=label, weight_norm='unit-noise-gain')
                 source_power.append(stc_source_power.data)
 
-    with warnings.catch_warnings(record=True):
-        stcs = tf_lcmv(epochs, forward, noise_covs, tmin, tmax, tstep,
-                       win_lengths, freq_bins, reg=reg, label=label)
+    assert_raises(ValueError, tf_lcmv, epochs, forward, noise_covs, tmin, tmax,
+                  tstep, win_lengths, freq_bins, reg=reg, label=label)
+    stcs = tf_lcmv(epochs, forward, noise_covs, tmin, tmax, tstep,
+                   win_lengths, freq_bins, reg=reg, label=label, raw=raw)
 
     assert_true(len(stcs) == len(freq_bins))
     assert_true(stcs[0].shape[1] == 4)
@@ -448,7 +449,7 @@ def test_tf_lcmv():
         # responses yields zeros
         stcs = tf_lcmv(epochs[0], forward, noise_covs, tmin, tmax, tstep,
                        win_lengths, freq_bins, subtract_evoked=True, reg=reg,
-                       label=label)
+                       label=label, raw=raw)
 
     assert_array_almost_equal(stcs[0].data, np.zeros_like(stcs[0].data))