Repository: mne-tools/mne-python

Issue:
Import ICA results from EEGLAB
#### Describe the problem
EEG .set files can contain ICA results. Related data are populated in fields icaweights, icasphere, and icawinv as far as I know. Although users can build mne.preprocessing.ICA objects by hand from these fields, as proposed in [here](https://mail.nmr.mgh.harvard.edu/pipermail//mne_analysis/2018-March/004781.html), this requires a good knowledge of the internals of MNE to deal correctly with pre-whitening and similar preprocessing steps such that the results obtained in EEGLAB are reproducible in MNE. It is also error-prone.

#### Describe your solution
To add an import function to MNE to import ICA components from EEGLAB .set files in a transparent manner. 


Discussion:
I'll take a look.
Let me know how it goes. I will need this pretty soon for my analyses so I can code it if needed and share the piece of code I'll come up with. But if you already have some useful code lying around, let me know. It would save me a bit of head-scratching!
A small tentative snippet of code.

```
from eegip.utils import loadmat
from mne.preprocessing import ICA
import numpy as np
from mne.io import read_raw_eeglab

file_name = '/project/def-emayada/eegip/london/derivatives/lossless/sub-s601/ses-m06/eeg/sub-s601_ses-m06_eeg_qcr.set'

def read_eeglab_ica(file_name):
    mat_file = loadmat(file_name)

    ica_ind = mat_file["EEG"]["chaninfo"]["icachansind"]-1
    ch_names = np.array([chan["labels"] for chan in mat_file["EEG"]["chanlocs"]])[ica_ind]


    ica = ICA(method='imported_eeglab', n_components=len(ch_names))  

    mat_file = loadmat(file_name)

    ica.current_fit = "eeglab"
    ica.ch_names = ch_names.tolist()
    ica.n_pca_components = None 
    ica.max_pca_components = len(ica.ch_names)
    ica.n_components_ = len(ica.ch_names)

    ica.pre_whitener_ = np.ones((ica.n_components_, 1))
    ica.pca_mean_ = np.zeros(ica.n_components_)
    ica.pca_components_ = mat_file["EEG"]["icasphere"]

    ica.unmixing_matrix_ = mat_file["EEG"]["icaweights"]
    ica.mixing_matrix_ = mat_file["EEG"]["icawinv"]
    
    return ica

raw_data = read_raw_eeglab(file_name, preload=True)
ica.apply(raw_data)
```

A few things. The loadmat function is just a version of `scipy.io.loadmat` that does a better job at reading mat files, in line of what is discussed here https://stackoverflow.com/a/29126361/1825043

Not sure I got right the MNE-EEGLAB correspondence for ICA properties. However, I am not even sure that it is possible to have a perfect correspondence because EEGLAB does not export variables expected by MNE such as the pca_mean_ values and because of differences in preprocessing (e.g., sphering versus whitening and such... as discussed in [here](https://github.com/mne-tools/mne-python/issues/4882) ). Anyway, this code runs but its output is not really tested yet. I'll work on coding tests to compare results in EEGLAB and MNE but I thought that I would post this preliminary snippet in case more knowledgeable people (e.g., @cbrnr, @agramfort, @larsoner ) have comments that could save me lot of time investigating obvious issues that I would take a long time to debug because of my limited knowledge about ICA algorithms and MNE internals. 
One hint is that we use `mne.externals.pymatreader.read_mat` to unify over old-style and new-style `.mat` files. Regarding the other stuff I'm not sure offhand, hopefully your investigations plus our documentation can make things clear.
Thanks for the hint @larsoner . I replaced eegip.utils.loadmat by mne.externals.pymatreader.read_mat and it works without issues with the rest of the snippet. I'll be happy to drop eegip.utils.loadmat and not have to care for this code anymore.
do yo plan to open a PR?

@agramfort I'll be happy to open a PR when I'll have something suffiently well tested if you think it can help.
Please do share what you have when you are happy about it 

I got some code that I'll prepare for PR. It has been tested using 
```
from mne.io import read_epochs_eeglab
from mne.preprocessing import read_eeglab_ica
from mne.externals.pymatreader import read_mat
import numpy as np
import os

mica_root = '/home/christian/Documents/mica_release/'
file_name = os.path.join(mica_root, 'datasets', 'ap82.set')
file_name_cleaned_matlab = os.path.join(mica_root, 'datasets', 'ap82.cleaned.set')

epochs = read_epochs_eeglab(file_name)
epochs_cleaned_matlab = read_epochs_eeglab(file_name_cleaned_matlab)

mark_to_remove = ["manual"]
mat_file = read_mat(file_name_cleaned_matlab)["EEG"]
comp_info = mat_file["marks"]["comp_info"]

if len(comp_info["flags"].shape) > 1:
    ind_comp_to_drop = np.unique(np.concatenate([np.where(flags)[0] for flags, label in zip(comp_info["flags"],
                                                                                            comp_info["label"])
                                                 if label in mark_to_remove]))
else:
    ind_comp_to_drop = np.where(comp_info["flags"])[0]

ica = read_eeglab_ica(file_name_cleaned_matlab)
eeg_epochs_cleaned = ica.apply(epochs.copy(), exclude=ind_comp_to_drop)

assert (np.all(np.isclose(epochs_cleaned_matlab.get_data(), eeg_epochs_cleaned.get_data())))
```
The file used for testing has been downloaded [here](ftp://sccn.ucsd.edu/pub/mica_release.zip) and processed in Matlab using 
```
file_name_in = '/home/christian/Documents/mica_release/datasets/ap82.set';
EEG = pop_loadset(file_name_in);
N_of_Components = 60;
[EEG.icaweights, EEG.icasphere] = runica(reshape(EEG.data,EEG.nbchan,EEG.pnts*EEG.trials), 'pca', N_of_Components);
EEG.icawinv    = [];
EEG.icaact     = [];

flags = zeros(N_of_Components, 1);
flags(5) = 1;
flags(32) = 1;
flags(43) = 1;
flags(56) = 1;
s = struct('label', 'manual', ...
           'line_color', [0.7000 0.7000 0.7000],...
           'order', 1,...
           'flags', flags);

EEG.marks.comp_info = [s];
EEG = eeg_checkset(EEG);

EEG = pop_marks_select_data(EEG,'component marks',[],'labels',{'manual'},'remove','on');
pop_saveset(EEG, 'filename', [file_name_in(1:end-4) '.cleaned.set']);
```
I wil prepare the PR but I wont include this test in it because of the dependance on the ap82.set and Matlab. A possibility would be to add both ap82.set and ap82.cleaned.set to the testing datasets of MNE but since it involves adding about 200 MB of data, I do not think it is a good idea. Something with a smaller file could probably be prepared. Alternatively, a test could depend only on ap82.set and a Matlab-python interface library like [this one](https://github.com/arokem/python-matlab-bridge) but I do not like the idea of adding an additionnal dependency and a dependency on Matlab. 
We already have these test files

https://github.com/mne-tools/mne-testing-data/tree/master/EEGLAB

If you adapt your code to run on these, then we don't have to add any new data.
... oh, except I guess the bits needed to reconstruct the `ICA` from what you compute in MATLAB

Resolving patch:
diff --git a/mne/preprocessing/__init__.py b/mne/preprocessing/__init__.py
--- a/mne/preprocessing/__init__.py
+++ b/mne/preprocessing/__init__.py
@@ -13,7 +13,8 @@
 from .eog import find_eog_events, create_eog_epochs
 from .ecg import find_ecg_events, create_ecg_epochs
 from .ica import (ICA, ica_find_eog_events, ica_find_ecg_events,
-                  get_score_funcs, read_ica, run_ica, corrmap)
+                  get_score_funcs, read_ica, run_ica, corrmap,
+                  read_ica_eeglab)
 from .otp import oversampled_temporal_projection
 from ._peak_finder import peak_finder
 from .bads import find_outliers
diff --git a/mne/preprocessing/ica.py b/mne/preprocessing/ica.py
--- a/mne/preprocessing/ica.py
+++ b/mne/preprocessing/ica.py
@@ -36,6 +36,8 @@
 from ..io.meas_info import write_meas_info, read_meas_info
 from ..io.constants import FIFF
 from ..io.base import BaseRaw
+from ..io.eeglab.eeglab import _get_info, _check_load_mat
+
 from ..epochs import BaseEpochs
 from ..viz import (plot_ica_components, plot_ica_scores,
                    plot_ica_sources, plot_ica_overlay)
@@ -52,15 +54,14 @@
                      _check_option, _PCA)
 from ..utils.check import _check_all_same_channel_names
 
-from ..fixes import _get_args
+from ..fixes import _get_args, _safe_svd
 from ..filter import filter_data
 from .bads import find_outliers
 from .ctps_ import ctps
 from ..io.pick import channel_type, pick_channels_regexp
 
-
 __all__ = ('ICA', 'ica_find_ecg_events', 'ica_find_eog_events',
-           'get_score_funcs', 'read_ica', 'run_ica')
+           'get_score_funcs', 'read_ica', 'run_ica', 'read_ica_eeglab')
 
 
 def _make_xy_sfunc(func, ndim_output=False):
@@ -194,7 +195,7 @@ class ICA(ContainsMixin):
         Channel names resulting from initial picking.
     n_components_ : int
         If fit, the actual number of PCA components used for ICA decomposition.
-    pre_whitener_ : ndarray, shape (n_channels, 1)
+    pre_whitener_ : ndarray, shape (n_channels, 1) or (n_channels, n_channels)
         If fit, array used to pre-whiten the data prior to PCA.
     pca_components_ : ndarray, shape (`max_pca_components`, n_channels)
         If fit, the PCA components.
@@ -213,7 +214,8 @@ class ICA(ContainsMixin):
     unmixing_matrix_ : ndarray, shape (`n_components_`, `n_components_`)
         If fit, the whitened matrix to go from PCA space to ICA space.
         Used, in combination with the `pca_components_`, by the methods
-        :meth:`ICA.get_sources` and :meth:`ICA.apply` to unmix the observed data.
+        :meth:`ICA.get_sources` and :meth:`ICA.apply` to unmix the observed
+        data.
     exclude : array-like of int
         List or np.array of sources indices to exclude when re-mixing the data
         in the :meth:`ICA.apply` method, i.e. artifactual ICA components.
@@ -326,7 +328,9 @@ def __init__(self, n_components=None, max_pca_components=None,
                  n_pca_components=None, noise_cov=None, random_state=None,
                  method='fastica', fit_params=None, max_iter=200,
                  allow_ref_meg=False, verbose=None):  # noqa: D102
-        _check_option('method', method, ['fastica', 'infomax', 'picard'])
+        _validate_type(method, str, 'method')
+        if method != 'imported_eeglab':  # internal use only
+            _check_option('method', method, ['fastica', 'infomax', 'picard'])
         if method == 'fastica' and not check_version('sklearn', '0.15'):
             raise RuntimeError('The scikit-learn package (version >= 0.15) '
                                'is required for FastICA.')
@@ -626,42 +630,41 @@ def _fit(self, data, max_pca_components, fit_type):
         """Aux function."""
         random_state = check_random_state(self.random_state)
         pca = _PCA(n_components=max_pca_components, whiten=True)
+        n_channels, n_samples = data.shape
         data = pca.fit_transform(data.T)
+        assert data.shape == (n_samples, max_pca_components or n_channels)
 
         if isinstance(self.n_components, float):
-            n_components_ = np.sum(pca.explained_variance_ratio_.cumsum() <=
-                                   self.n_components)
-            if n_components_ < 1:
+            self.n_components_ = np.sum(
+                pca.explained_variance_ratio_.cumsum() <= self.n_components)
+            if self.n_components_ < 1:
                 raise RuntimeError('One PCA component captures most of the '
                                    'explained variance, your threshold resu'
                                    'lts in 0 components. You should select '
                                    'a higher value.')
-            logger.info('Selection by explained variance: %i components' %
-                        n_components_)
-            sel = slice(n_components_)
+            msg = 'Selecting by explained variance'
         else:
             if self.n_components is not None:  # normal n case
-                sel = slice(self.n_components)
-                logger.info('Selection by number: %i components' %
-                            self.n_components)
+                self.n_components_ = _ensure_int(self.n_components)
+                msg = 'Selecting by number'
             else:  # None case
-                logger.info('Using all PCA components: %i'
-                            % len(pca.components_))
-                sel = slice(len(pca.components_))
+                self.n_components_ = len(pca.components_)
+                msg = 'Selecting all PCA components'
+        logger.info('%s: %s components' % (msg, self.n_components_))
 
         # the things to store for PCA
         self.pca_mean_ = pca.mean_
         self.pca_components_ = pca.components_
-        self.pca_explained_variance_ = exp_var = pca.explained_variance_
+        self.pca_explained_variance_ = pca.explained_variance_
         del pca
         # update number of components
-        self.n_components_ = sel.stop
         self._update_ica_names()
         if self.n_pca_components is not None:
             if self.n_pca_components > len(self.pca_components_):
                 self.n_pca_components = len(self.pca_components_)
 
         # take care of ICA
+        sel = slice(0, self.n_components_)
         if self.method == 'fastica':
             from sklearn.decomposition import FastICA
             ica = FastICA(whiten=False, random_state=random_state,
@@ -678,10 +681,15 @@ def _fit(self, data, max_pca_components, fit_type):
                              random_state=random_state, **self.fit_params)
             del _
             self.unmixing_matrix_ = W
-        self.unmixing_matrix_ /= np.sqrt(exp_var[sel])[None, :]  # whitening
-        self.mixing_matrix_ = linalg.pinv(self.unmixing_matrix_)
+        assert self.unmixing_matrix_.shape == (self.n_components_,) * 2
+        self.unmixing_matrix_ /= np.sqrt(
+            self.pca_explained_variance_[sel])[None, :]  # whitening
+        self._update_mixing_matrix()
         self.current_fit = fit_type
 
+    def _update_mixing_matrix(self):
+        self.mixing_matrix_ = linalg.pinv(self.unmixing_matrix_)
+
     def _update_ica_names(self):
         """Update ICA names when n_components_ is set."""
         self._ica_names = ['ICA%03d' % ii for ii in range(self.n_components_)]
@@ -1466,42 +1474,53 @@ def _pick_sources(self, data, include, exclude):
         """Aux function."""
         exclude = self._check_exclude(exclude)
         _n_pca_comp = self._check_n_pca_components(self.n_pca_components)
+        n_ch, _ = data.shape
 
         if not(self.n_components_ <= _n_pca_comp <= self.max_pca_components):
             raise ValueError('n_pca_components must be >= '
                              'n_components and <= max_pca_components.')
 
-        n_components = self.n_components_
-        logger.info('Transforming to ICA space (%i components)' % n_components)
+        logger.info('Transforming to ICA space (%i components)'
+                    % self.n_components_)
 
         # Apply first PCA
         if self.pca_mean_ is not None:
             data -= self.pca_mean_[:, None]
 
-        sel_keep = np.arange(n_components)
+        sel_keep = np.arange(self.n_components_)
         if include not in (None, []):
             sel_keep = np.unique(include)
         elif exclude not in (None, []):
-            sel_keep = np.setdiff1d(np.arange(n_components), exclude)
-
-        logger.info('Zeroing out %i ICA components'
-                    % (n_components - len(sel_keep)))
-
+            sel_keep = np.setdiff1d(np.arange(self.n_components_), exclude)
+
+        n_zero = self.n_components_ - len(sel_keep)
+        logger.info('Zeroing out %i ICA component%s' % (n_zero, _pl(n_zero)))
+
+        # Mixing and unmixing should both be shape (self.n_components_, 2),
+        # and we need to put these into the upper left part of larger mixing
+        # and unmixing matrices of shape (n_ch, _n_pca_comp)
+        pca_components = self.pca_components_[:_n_pca_comp]
+        assert pca_components.shape == (_n_pca_comp, n_ch)
+        assert self.unmixing_matrix_.shape == \
+            self.mixing_matrix_.shape == \
+            (self.n_components_,) * 2
         unmixing = np.eye(_n_pca_comp)
-        unmixing[:n_components, :n_components] = self.unmixing_matrix_
-        unmixing = np.dot(unmixing, self.pca_components_[:_n_pca_comp])
+        unmixing[:self.n_components_, :self.n_components_] = \
+            self.unmixing_matrix_
+        unmixing = np.dot(unmixing, pca_components)
 
         mixing = np.eye(_n_pca_comp)
-        mixing[:n_components, :n_components] = self.mixing_matrix_
-        mixing = np.dot(self.pca_components_[:_n_pca_comp].T, mixing)
-
-        if _n_pca_comp > n_components:
-            sel_keep = np.concatenate(
-                (sel_keep, range(n_components, _n_pca_comp)))
-
+        mixing[:self.n_components_, :self.n_components_] = \
+            self.mixing_matrix_
+        mixing = pca_components.T @ mixing
+        assert mixing.shape == unmixing.shape[::-1] == (n_ch, _n_pca_comp)
+
+        # keep requested components plus residuals (if any)
+        sel_keep = np.concatenate(
+            (sel_keep, np.arange(self.n_components_, _n_pca_comp)))
         proj_mat = np.dot(mixing[:, sel_keep], unmixing[sel_keep, :])
-
         data = np.dot(proj_mat, data)
+        assert proj_mat.shape == (n_ch,) * 2
 
         if self.pca_mean_ is not None:
             data += self.pca_mean_[:, None]
@@ -2135,7 +2154,7 @@ def f(x):
     ica._update_ica_names()
     ica.pca_explained_variance_ = f(pca_explained_variance)
     ica.unmixing_matrix_ = f(unmixing_matrix)
-    ica.mixing_matrix_ = linalg.pinv(ica.unmixing_matrix_)
+    ica._update_mixing_matrix()
     ica.exclude = [] if exclude is None else list(exclude)
     ica.info = info
     if 'n_samples_' in ica_misc:
@@ -2622,3 +2641,50 @@ def corrmap(icas, template, threshold="auto", label=None, ch_type="eeg",
         return template_fig, labelled_ics
     else:
         return None
+
+
+def read_ica_eeglab(fname):
+    """Load ICA information saved in an EEGLAB .set file.
+
+    Parameters
+    ----------
+    fname : str
+        Complete path to a .set EEGLAB file that contains an ICA object.
+
+    Returns
+    -------
+    ica : instance of ICA
+        An ICA object based on the information contained in the input file.
+    """
+    eeg = _check_load_mat(fname, None)
+    info = _get_info(eeg)[0]
+    pick_info(info, np.round(eeg['icachansind']).astype(int) - 1, copy=False)
+
+    n_components = eeg.icaweights.shape[0]
+
+    ica = ICA(method='imported_eeglab', n_components=n_components)
+
+    ica.current_fit = "eeglab"
+    ica.ch_names = info["ch_names"]
+    ica.n_pca_components = None
+    ica.max_pca_components = n_components
+    ica.n_components_ = n_components
+
+    ica.pre_whitener_ = np.ones((len(eeg.icachansind), 1))
+    ica.pca_mean_ = np.zeros(len(eeg.icachansind))
+
+    n_ch = len(ica.ch_names)
+    assert eeg.icaweights.shape == (n_components, n_ch)
+    if n_components < n_ch:
+        # When PCA reduction is used in EEGLAB, runica returns
+        # weights= weights*sphere*eigenvectors(:,1:ncomps)';
+        # sphere = eye(urchans), so let's use SVD to get our square
+        # weights matrix (u * s) and our PCA vectors (v) back
+        u, s, v = _safe_svd(eeg.icaweights, full_matrices=False)
+        ica.unmixing_matrix_ = u * s
+        ica.pca_components_ = v
+    else:
+        ica.unmixing_matrix_ = eeg.icaweights
+        ica.pca_components_ = eeg.icasphere
+    ica._update_mixing_matrix()
+    return ica


Test patch:
diff --git a/mne/preprocessing/tests/test_ica.py b/mne/preprocessing/tests/test_ica.py
--- a/mne/preprocessing/tests/test_ica.py
+++ b/mne/preprocessing/tests/test_ica.py
@@ -12,7 +12,7 @@
 import numpy as np
 from numpy.testing import (assert_array_almost_equal, assert_array_equal,
                            assert_allclose, assert_equal)
-from scipy import stats
+from scipy import stats, linalg
 import matplotlib.pyplot as plt
 
 from mne import (Epochs, read_events, pick_types, create_info, EpochsArray,
@@ -21,10 +21,11 @@
 from mne.preprocessing import (ICA, ica_find_ecg_events, ica_find_eog_events,
                                read_ica, run_ica)
 from mne.preprocessing.ica import (get_score_funcs, corrmap, _sort_components,
-                                   _ica_explained_variance)
+                                   _ica_explained_variance, read_ica_eeglab)
 from mne.io import read_raw_fif, Info, RawArray, read_raw_ctf, read_raw_eeglab
 from mne.io.meas_info import _kind_dict
 from mne.io.pick import _DATA_CH_TYPES_SPLIT
+from mne.io.eeglab.eeglab import _check_load_mat
 from mne.rank import _compute_rank_int
 from mne.utils import (catch_logging, _TempDir, requires_sklearn,
                        run_tests_if_main)
@@ -89,6 +90,7 @@ def test_ica_full_data_recovery(method):
                       method=method, max_iter=1)
             with pytest.warns(UserWarning, match=None):  # sometimes warns
                 ica.fit(raw, picks=list(range(n_channels)))
+            _assert_ica_attributes(ica)
             raw2 = ica.apply(raw.copy(), exclude=[])
             if ok:
                 assert_allclose(data[:n_channels], raw2._data[:n_channels],
@@ -102,6 +104,7 @@ def test_ica_full_data_recovery(method):
                       n_pca_components=n_pca_components, random_state=0)
             with pytest.warns(None):  # sometimes warns
                 ica.fit(epochs, picks=list(range(n_channels)))
+            _assert_ica_attributes(ica)
             epochs2 = ica.apply(epochs.copy(), exclude=[])
             data2 = epochs2.get_data()[:, :n_channels]
             if ok:
@@ -293,6 +296,7 @@ def test_ica_core(method):
                   random_state=0, method=method)
         with pytest.warns(None):  # sometimes warns
             ica.fit(epochs, picks=picks)
+        _assert_ica_attributes(ica)
         data = epochs.get_data()[:, 0, :]
         n_samples = np.prod(data.shape)
         assert_equal(ica.n_samples_, n_samples)
@@ -354,6 +358,7 @@ def test_ica_additional(method):
               n_pca_components=None, random_state=0, method=method, max_iter=1)
     with pytest.warns(UserWarning, match='did not converge'):
         ica.fit(epochs)
+    _assert_ica_attributes(ica)
     # for testing eog functionality
     picks2 = np.concatenate([picks, pick_types(raw.info, False, eog=True)])
     epochs_eog = Epochs(raw, events[:4], event_id, tmin, tmax, picks=picks2,
@@ -366,6 +371,7 @@ def test_ica_additional(method):
     assert (ica.info is None)
     with pytest.warns(RuntimeWarning, match='normalize_proj'):
         ica.fit(raw, picks[:5])
+    _assert_ica_attributes(ica)
     assert (isinstance(ica.info, Info))
     assert (ica.n_components_ < 5)
 
@@ -374,6 +380,7 @@ def test_ica_additional(method):
     pytest.raises(RuntimeError, ica.save, '')
 
     ica.fit(raw, picks=[1, 2, 3, 4, 5], start=start, stop=stop2)
+    _assert_ica_attributes(ica)
 
     # check passing a ch_name to find_bads_ecg
     with pytest.warns(RuntimeWarning, match='longer'):
@@ -436,6 +443,7 @@ def test_ica_additional(method):
     n_samples = raw_._data.shape[1]
     with pytest.warns(UserWarning, match='did not converge'):
         ica.fit(raw, picks=picks[:5], decim=3)
+    _assert_ica_attributes(ica)
     assert raw_._data.shape[1] == n_samples
 
     # test expl var
@@ -443,6 +451,7 @@ def test_ica_additional(method):
               n_pca_components=4, method=method, max_iter=1)
     with pytest.warns(UserWarning, match='did not converge'):
         ica.fit(raw, picks=None, decim=3)
+    _assert_ica_attributes(ica)
     assert (ica.n_components_ == 4)
     ica_var = _ica_explained_variance(ica, raw, normalize=True)
     assert (np.all(ica_var[:-1] >= ica_var[1:]))
@@ -463,6 +472,7 @@ def test_ica_additional(method):
                   n_pca_components=4, method=method, max_iter=1)
         with pytest.warns(None):  # ICA does not converge
             ica.fit(raw, picks=picks[:10], start=start, stop=stop2)
+        _assert_ica_attributes(ica)
         sources = ica.get_sources(epochs).get_data()
         assert (ica.mixing_matrix_.shape == (2, 2))
         assert (ica.unmixing_matrix_.shape == (2, 2))
@@ -682,6 +692,7 @@ def f(x, y):
     ica = ICA(method=method)
     with pytest.warns(None):  # sometimes does not converge
         ica.fit(raw, picks=picks[:5])
+    _assert_ica_attributes(ica)
     with pytest.warns(RuntimeWarning, match='longer'):
         ica.find_bads_ecg(raw)
     ica.find_bads_eog(epochs, ch_name='MEG 0121')
@@ -725,6 +736,7 @@ def test_ica_reject_buffer(method):
         assert (raw._data[:5, ::2].shape[1] - 4 == ica.n_samples_)
     log = [l for l in drop_log.getvalue().split('\n') if 'detected' in l]
     assert_equal(len(log), 1)
+    _assert_ica_attributes(ica)
 
 
 @requires_sklearn
@@ -827,11 +839,13 @@ def test_eog_channel(method):
         picks1 = np.append(picks1a, picks1b)
         ica.fit(inst, picks=picks1)
         assert (any('EOG' in ch for ch in ica.ch_names))
+        _assert_ica_attributes(ica)
     # Test case for MEG data. Should have no EOG channel
     for inst in [raw, epochs]:
         picks1 = pick_types(inst.info, meg=True, stim=False, ecg=False,
                             eog=False, exclude='bads')[:5]
         ica.fit(inst, picks=picks1)
+        _assert_ica_attributes(ica)
         assert not any('EOG' in ch for ch in ica.ch_names)
 
 
@@ -856,6 +870,7 @@ def test_max_pca_components_none(method):
               n_components=n_components, random_state=random_state)
     with pytest.warns(None):
         ica.fit(epochs)
+    _assert_ica_attributes(ica)
     ica.save(output_fname)
 
     ica = read_ica(output_fname)
@@ -887,9 +902,11 @@ def test_n_components_none(method):
               n_components=n_components, random_state=random_state)
     with pytest.warns(None):
         ica.fit(epochs)
+    _assert_ica_attributes(ica)
     ica.save(output_fname)
 
     ica = read_ica(output_fname)
+    _assert_ica_attributes(ica)
 
     # ICA.fit() replaced max_pca_components, which was previously None,
     # with the appropriate integer value.
@@ -919,8 +936,10 @@ def test_n_components_and_max_pca_components_none(method):
     with pytest.warns(None):  # convergence
         ica.fit(epochs)
     ica.save(output_fname)
+    _assert_ica_attributes(ica)
 
     ica = read_ica(output_fname)
+    _assert_ica_attributes(ica)
 
     # ICA.fit() replaced max_pca_components, which was previously None,
     # with the appropriate integer value.
@@ -946,6 +965,7 @@ def test_ica_ctf():
                       method=method)
             with pytest.warns(UserWarning, match='did not converge'):
                 ica.fit(inst)
+            _assert_ica_attributes(ica)
 
         # test apply and get_sources
         for inst in [raw, epochs, evoked]:
@@ -957,6 +977,7 @@ def test_ica_ctf():
     ica = ICA(n_components=2, random_state=0, max_iter=2, method=method)
     with pytest.warns(UserWarning, match='did not converge'):
         ica.fit(raw)
+    _assert_ica_attributes(ica)
     raw.apply_gradient_compensation(1)
     epochs = Epochs(raw, events, None, -0.2, 0.2, preload=True)
     evoked = epochs.average()
@@ -988,6 +1009,7 @@ def test_ica_labels():
     ica = ICA(n_components=4, random_state=0, max_iter=2, method='fastica')
     with pytest.warns(UserWarning, match='did not converge'):
         ica.fit(raw)
+    _assert_ica_attributes(ica)
 
     ica.find_bads_eog(raw, l_freq=None, h_freq=None)
     picks = list(pick_types(raw.info, meg=False, eog=True))
@@ -1048,6 +1070,7 @@ def test_ica_eeg():
                           method=method)
                 with pytest.warns(None):
                     ica.fit(inst, picks=picks)
+                _assert_ica_attributes(ica)
 
             # test apply and get_sources
             for inst in [raw, epochs, evoked]:
@@ -1075,6 +1098,7 @@ def test_ica_eeg():
                           method=method)
                 with pytest.warns(None):
                     ica.fit(inst)
+                _assert_ica_attributes(ica)
 
             # test apply and get_sources
             for inst in [raw, epochs, evoked]:
@@ -1082,4 +1106,67 @@ def test_ica_eeg():
                 ica.get_sources(inst)
 
 
+@testing.requires_testing_data
+def test_read_ica_eeglab():
+    """Test read_ica_eeglab function."""
+    fname = op.join(test_base_dir, "EEGLAB", "test_raw.set")
+    fname_cleaned_matlab = op.join(test_base_dir, "EEGLAB",
+                                   "test_raw.cleaned.set")
+
+    raw = read_raw_eeglab(fname, preload=True)
+    raw_eeg = _check_load_mat(fname, None)
+    raw_cleaned_matlab = read_raw_eeglab(fname_cleaned_matlab,
+                                         preload=True)
+
+    mark_to_remove = ["manual"]
+    comp_info = raw_eeg.marks["comp_info"]
+
+    if len(comp_info["flags"].shape) > 1:
+        ind_comp_to_drop = [np.where(flags)[0]
+                            for flags, label in zip(comp_info["flags"],
+                                                    comp_info["label"])
+                            if label in mark_to_remove]
+        ind_comp_to_drop = np.unique(np.concatenate(ind_comp_to_drop))
+    else:
+        ind_comp_to_drop = np.where(comp_info["flags"])[0]
+
+    ica = read_ica_eeglab(fname)
+    _assert_ica_attributes(ica)
+    raw_cleaned = ica.apply(raw.copy(), exclude=ind_comp_to_drop)
+
+    assert_allclose(raw_cleaned_matlab.get_data(), raw_cleaned.get_data(),
+                    rtol=1e-05, atol=1e-08)
+
+
+def _assert_ica_attributes(ica):
+    """Assert some attributes of ICA objects."""
+    __tracebackhide__ = True
+    # This tests properties, but also serves as documentation of
+    # the shapes these arrays can obtain and how they obtain them
+
+    # Pre-whitener
+    n_ch = len(ica.ch_names)
+    assert ica.pre_whitener_.shape == (
+        n_ch, n_ch if ica.noise_cov is not None else 1)
+
+    # PCA
+    n_pca = ica.max_pca_components
+    assert ica.pca_components_.shape == (n_pca, n_ch), 'PCA shape'
+    assert_allclose(np.dot(ica.pca_components_, ica.pca_components_.T),
+                    np.eye(n_pca), atol=1e-6, err_msg='PCA orthogonality')
+    assert ica.pca_mean_.shape == (n_ch,)
+
+    # Mixing/unmixing
+    assert ica.unmixing_matrix_.shape == (ica.n_components_,) * 2, \
+        'Unmixing shape'
+    assert ica.mixing_matrix_.shape == (ica.n_components_,) * 2, \
+        'Mixing shape'
+    mix_unmix = np.dot(ica.mixing_matrix_, ica.unmixing_matrix_)
+    s = linalg.svdvals(ica.unmixing_matrix_)
+    nz = len(s) - (s > s[0] * 1e-12).sum()
+    want = np.eye(ica.n_components_)
+    want[:nz] = 0
+    assert_allclose(mix_unmix, want, atol=1e-6, err_msg='Mixing as pinv')
+
+
 run_tests_if_main()