Repository: mne-tools/mne-python

Issue:
ENH: EpochsArray
like RawArray but from 3D data cc @fraimondo @agramfort @Eric89GXL 



Discussion:
Should be very straightforward given the `RawArray` class and info-creation tool. +1

If I'm not mistaken @fraimondo is hacking on this right now (just 5 meters away from my desk) ;-)

``` Python
class EpochsArray(mne.epochs.Epochs):

    def __init__(self, data, info, events, event_id=None, time0=0):
        self.info = info
        self._data = data
        if event_id is None:  # convert to int to make typing-checks happy
            event_id = dict((str(e), int(e)) for e in np.unique(events[:, 2]))
        self.event_id = event_id
        self.events = events
        self.proj = None
        self.baseline = None
        self.preload = True
        self.reject = None
        self.decim = 0
        self.raw = None
        self.drop_log = []
        self.drop = True
        self.selection = np.arange(0, self.info['nchan'])
        self.picks = _check_type_picks(list(range(len(self.info['ch_names']))))
        self.times = np.arange(0, data.shape[2], dtype=np.float) / \
                     info['sfreq'] + time0
        self.tmin = self.times[0]
        self.tmax = self.times[-1]
```

This first draft looks like it works: saving, averaging, plotting.

PR welcome!!! 

We should briefly discuss the API. I would rename `time0` `t_zero` or so. Apart from that the parameters make sense to me. A question is whether we can allow for other than preloaded / preprocessed data. Would actually be lovely to be able to use this constructor with unprocessed data. This would mean, however, to add the reject api and to declare somehow at the beginning whether the data is dropped / to be rejected or not. 

Any thoughts @Eric89GXL @mluessi @agramfort ?

I think `tmin` would be most appropriate, since that's used elsewhere. There could just be a `reject=None` parameter that, if it's not `None`, does rejection. (same with `flat`). Or even better, to avoid parameter bloat, just add a method for this class like `reject(eeg=100e-6, ...)` or something.

> I think tmin ...

yes indeed. 

> There could just be a reject=None parameter that, if it's not None, does rejection.

Yes.

@fraimondo PR? ^^

+1 for the PR


Resolving patch:
diff --git a/mne/epochs.py b/mne/epochs.py
--- a/mne/epochs.py
+++ b/mne/epochs.py
@@ -1549,6 +1549,72 @@ def equalize_event_counts(self, event_ids, method='mintime', copy=True):
         return epochs, indices
 
 
+class EpochsArray(Epochs):
+    """Epochs object from numpy array
+
+    Parameters
+    ----------
+    data : array, shape (n_epochs, n_channels, n_times)
+        The channels' time series for each epoch.
+    info : instance of Info
+        Info dictionary. Consider using ``create_info`` to populate
+        this structure.
+    events : array, shape (n_events, 3)
+        The events typically returned by the read_events function.
+        If some events don't match the events of interest as specified
+        by event_id, they will be marked as 'IGNORED' in the drop log.
+    tmin : float
+        Start time before event.
+    event_id : int | list of int | dict | None
+        The id of the event to consider. If dict,
+        the keys can later be used to acces associated events. Example:
+        dict(auditory=1, visual=3). If int, a dict will be created with
+        the id as string. If a list, all events with the IDs specified
+        in the list are used. If None, all events will be used with
+        and a dict is created with string integer names corresponding
+        to the event id integers.
+    verbose : bool, str, int, or None
+        If not None, override default verbose level (see mne.verbose).
+        Defaults to raw.verbose.
+    """
+
+    @verbose
+    def __init__(self, data, info, events, event_id=None, tmin=0,
+                 verbose=None):
+        self.info = info
+        self._data = data
+        if event_id is None:  # convert to int to make typing-checks happy
+            event_id = dict((str(e), int(e)) for e in np.unique(events[:, 2]))
+        self.event_id = event_id
+        self.events = events
+
+        for key, val in self.event_id.items():
+            if val not in events[:, 2]:
+                msg = ('No matching events found for %s '
+                       '(event id %i)' % (key, val))
+                raise ValueError(msg)
+
+        self.proj = None
+        self.baseline = None
+        self.preload = True
+        self.reject = None
+        self.decim = 1
+        self._decim_idx = slice(0, data.shape[-1], self.decim)
+        self.raw = None
+        self.drop_log = [[] for _ in range(len(events))]
+        self._bad_dropped = True
+
+        self.selection = np.arange(len(events))
+        self.picks = None
+        self.times = (np.arange(data.shape[-1], dtype=np.float) /
+                                info['sfreq'] + tmin)
+        self.tmin = self.times[0]
+        self.tmax = self.times[-1]
+        self.verbose = verbose
+        self.name = 'Unknown'
+        self._projector = None
+
+
 def combine_event_ids(epochs, old_event_ids, new_event_id, copy=True):
     """Collapse event_ids from an epochs instance into a new event_id
 
diff --git a/mne/io/array/array.py b/mne/io/array/array.py
--- a/mne/io/array/array.py
+++ b/mne/io/array/array.py
@@ -1,3 +1,11 @@
+
+"""Tools for creating MNE objects from numpy arrays"""
+
+# Authors: Eric Larson <larson.eric.d@gmail.com>
+#          Denis Engemann <denis.engemann@gmail.com>
+#
+# License: BSD (3-clause)
+
 import numpy as np
 
 from ...constants import FIFF


Test patch:
diff --git a/mne/io/array/tests/test_array.py b/mne/io/array/tests/test_array.py
--- a/mne/io/array/tests/test_array.py
+++ b/mne/io/array/tests/test_array.py
@@ -7,12 +7,12 @@
 import os.path as op
 import warnings
 
-from numpy.testing import assert_array_almost_equal, assert_allclose
+from numpy.testing import (assert_array_almost_equal, assert_allclose,
+                           assert_array_equal)
 from nose.tools import assert_equal, assert_raises, assert_true
-
 from mne import find_events, Epochs, pick_types
 from mne.io import Raw
-from mne.io.array import RawArray, create_info
+from mne.io.array import create_info, RawArray
 from mne.utils import _TempDir
 
 warnings.simplefilter('always')  # enable b/c these tests might throw warnings
@@ -23,7 +23,7 @@
 tempdir = _TempDir()
 
 
-def test_array():
+def test_array_raw():
     """Test creating raw from array
     """
     # creating
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
@@ -17,11 +17,12 @@
 from mne import (io, Epochs, read_events, pick_events, read_epochs,
                  equalize_channels, pick_types, pick_channels)
 from mne.epochs import (bootstrap, equalize_epoch_counts, combine_event_ids,
-                        add_channels_epochs)
+                        add_channels_epochs, EpochsArray)
 from mne.utils import (_TempDir, requires_pandas, requires_nitime,
                        clean_warning_registry)
 
 from mne.io import read_evokeds
+from mne.io.array import create_info
 from mne.io.proj import _has_eeg_average_ref_proj
 from mne.event import merge_events
 from mne.constants import FIFF
@@ -39,8 +40,8 @@
 raw = io.Raw(raw_fname, add_eeg_ref=False)
 events = read_events(event_name)
 picks = pick_types(raw.info, meg=True, eeg=True, stim=True,
-                        ecg=True, eog=True, include=['STI 014'],
-                        exclude='bads')
+                   ecg=True, eog=True, include=['STI 014'],
+                   exclude='bads')
 
 reject = dict(grad=1000e-12, mag=4e-12, eeg=80e-6, eog=150e-6)
 flat = dict(grad=1e-15, mag=1e-15)
@@ -1108,3 +1109,42 @@ def make_epochs(picks):
     epochs_meg2.event_id['b'] = 2
     assert_raises(NotImplementedError, add_channels_epochs,
                   [epochs_meg2, epochs_eeg])
+
+
+def test_array_epochs():
+    """Test creating epochs from array
+    """
+
+    # creating
+    rng = np.random.RandomState(42)
+    data = rng.random_sample((10, 20, 50))
+    sfreq = 1e3
+    ch_names = ['EEG %03d' % (i + 1) for i in range(20)]
+    types = ['eeg'] * 20
+    info = create_info(ch_names, sfreq, types)
+    events = np.c_[np.arange(1, 600, 60),
+                   np.zeros(10),
+                   [1, 2] * 5]
+    event_id = {'a': 1, 'b': 2}
+    epochs = EpochsArray(data, info, events=events, event_id=event_id,
+                         tmin=-.2)
+
+    # saving
+    temp_fname = op.join(tempdir, 'epo.fif')
+    epochs.save(temp_fname)
+    epochs2 = read_epochs(temp_fname)
+    data2 = epochs2.get_data()
+    assert_allclose(data, data2)
+    assert_allclose(epochs.times, epochs2.times)
+    assert_equal(epochs.event_id, epochs2.event_id)
+    assert_array_equal(epochs.events, epochs2.events)
+
+    # plotting
+    import matplotlib
+    matplotlib.use('Agg')  # for testing don't use X server
+    epochs[0].plot()
+
+    # indexing
+    assert_array_equal(np.unique(epochs['a'].events[:, 2]), np.array([1]))
+    assert_equal(len(epochs[:2]), 2)
+