Repository: mne-tools/mne-python

Issue:
Read ICA EEGLab: problems with AMICA decomposition 

### (possible) Bug

`read_ica_eeglab()` appears to not be able to read in AMICA (note: *not* `runica`) decompositions from an EEGLab .set file.

The error upon trying to instantiate ICA:

```
  AssertionError                            Traceback (most recent call last)
 <ipython-input-10-a6f5a4e0501a> in <module>
       5 from mne import preprocessing
       6 
 ----> 7 ica = preprocessing.read_ica_eeglab('full_filepath')
       8 
       9 
 
 ~/opt/anaconda3/envs/mne/lib/python3.8/site-packages/mne/preprocessing/ica.py in read_ica_eeglab(fname)
    2754 
    2755     n_ch = len(ica.ch_names)
 -> 2756     assert eeg.icaweights.shape == (n_components, n_ch)
    2757     # When PCA reduction is used in EEGLAB, runica returns
    2758     # weights= weights*sphere*eigenvectors(:,1:ncomps)';
 
 AssertionError: 
```



### Steps to reproduce
 ICA was performed with EEGLab's [AMICA](https://github.com/japalmer29/amica/wiki/AMICA) (not runica) with PCA reduction to rank correct for interpolated channels.

Looking at the data in EEGLab, `ICAweights = n_components x n_pcs`, and `ICAsphere = n_pcs x n_channels`. I’m guessing `read_ica_eeglab` is assuming the `icaweights` matrix is already multiplied by `icasphere`.


### Expected results
`read_ica_eeglab` should correctly read in pca reduced IC decompositions from AMICA (and possibly other algorithms -- this might want to be tested, such as FASTICA).

### Minimal reproducible example
[61 channel recording (.set/.fdt)](https://github.com/mne-tools/mne-python/files/5822735/MNE_TestData.zip)




Discussion:
@christian-oreilly do you have time to take a look?
@cbrnr I cannot for the next week because I have coming deadlines, but I'll have a look at it after that.
Thanks! Also should note that IC rejection already occurred in this dataset (and all the datasets I’m planning to use). 
I don't think AMICA is supported in mne right now. For example AMICA allows for multiple ICA models and provides a timecourse for their likelihood. Some of the ICs can be even shared between these models. Because of these differences mne-python could be able to import AMICA model only in the simplest scenario. But you are right - there might be an inconsistency in how ICA is read from eeglab in general, expecting no PCA reduction. 
Hi, to clarify - the request is not to have functionality for reading in AMICA direct output/models. The expectation (and my problem) is that the appropriate models and time courses were selected using the postAmicaUtility in EEGLAB and single icaweights and icasphere matrices are in the .set file. Perhaps this the simplest scenario you had in mind?
@michaelcgold Yes, that should be the simplest scenario. :) In other words, your problem does not seem specific to AMICA (at least at the surface).

Resolving patch:
diff --git a/mne/io/eeglab/eeglab.py b/mne/io/eeglab/eeglab.py
--- a/mne/io/eeglab/eeglab.py
+++ b/mne/io/eeglab/eeglab.py
@@ -323,7 +323,7 @@ def __init__(self, input_fname, eog=(),
                             ' the .set file contains epochs.' % eeg.trials)
 
         last_samps = [eeg.pnts - 1]
-        info, eeg_montage, update_ch_names = _get_info(eeg, eog=eog)
+        info, eeg_montage, _ = _get_info(eeg, eog=eog)
 
         # read the data
         if isinstance(eeg.data, str):
diff --git a/mne/preprocessing/ica.py b/mne/preprocessing/ica.py
--- a/mne/preprocessing/ica.py
+++ b/mne/preprocessing/ica.py
@@ -53,8 +53,8 @@
                      compute_corr, _get_inst_data, _ensure_int,
                      copy_function_doc_to_method_doc, _pl, warn, Bunch,
                      _check_preload, _check_compensation_grade, fill_doc,
-                     _check_option, _PCA, int_like)
-from ..utils.check import _check_all_same_channel_names
+                     _check_option, _PCA, int_like,
+                     _check_all_same_channel_names)
 
 from ..fixes import _get_args, _safe_svd
 from ..filter import filter_data
@@ -2678,13 +2678,15 @@ def corrmap(icas, template, threshold="auto", label=None, ch_type="eeg",
         return None
 
 
-def read_ica_eeglab(fname):
+@verbose
+def read_ica_eeglab(fname, *, verbose=None):
     """Load ICA information saved in an EEGLAB .set file.
 
     Parameters
     ----------
     fname : str
         Complete path to a .set EEGLAB file that contains an ICA object.
+    %(verbose)s
 
     Returns
     -------
@@ -2692,9 +2694,11 @@ def read_ica_eeglab(fname):
         An ICA object based on the information contained in the input file.
     """
     eeg = _check_load_mat(fname, None)
-    info = _get_info(eeg)[0]
+    info, eeg_montage, _ = _get_info(eeg)
+    info.set_montage(eeg_montage)
     pick_info(info, np.round(eeg['icachansind']).astype(int) - 1, copy=False)
 
+    rank = eeg.icasphere.shape[0]
     n_components = eeg.icaweights.shape[0]
 
     ica = ICA(method='imported_eeglab', n_components=n_components)
@@ -2704,11 +2708,15 @@ def read_ica_eeglab(fname):
     ica.n_pca_components = None
     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)
+    assert len(eeg.icachansind) == n_ch
+
+    ica.pre_whitener_ = np.ones((n_ch, 1))
+    ica.pca_mean_ = np.zeros(n_ch)
+
+    assert eeg.icasphere.shape[1] == n_ch
+    assert eeg.icaweights.shape == (n_components, rank)
+
     # When PCA reduction is used in EEGLAB, runica returns
     # weights= weights*sphere*eigenvectors(:,1:ncomps)';
     # sphere = eye(urchans). When PCA reduction is not used, we have:
@@ -2718,9 +2726,17 @@ def read_ica_eeglab(fname):
     # So in either case, we can use SVD to get our square whitened
     # weights matrix (u * s) and our PCA vectors (v) back:
     use = eeg.icaweights @ eeg.icasphere
+    use_check = linalg.pinv(eeg.icawinv)
+    if not np.allclose(use, use_check, rtol=1e-6):
+        warn('Mismatch between icawinv and icaweights @ icasphere from EEGLAB '
+             'possibly due to ICA component removal, assuming icawinv is '
+             'correct')
+        use = use_check
     u, s, v = _safe_svd(use, full_matrices=False)
     ica.unmixing_matrix_ = u * s
     ica.pca_components_ = v
     ica.pca_explained_variance_ = s * s
+    ica.info = info
     ica._update_mixing_matrix()
+    ica._update_ica_names()
     return ica
diff --git a/mne/utils/__init__.py b/mne/utils/__init__.py
--- a/mne/utils/__init__.py
+++ b/mne/utils/__init__.py
@@ -17,7 +17,8 @@
                     _check_path_like, _check_src_normal, _check_stc_units,
                     _check_pyqt5_version, _check_sphere, _check_time_format,
                     _check_freesurfer_home, _suggest, _require_version,
-                    _on_missing, int_like, _safe_input)
+                    _on_missing, int_like, _safe_input,
+                    _check_all_same_channel_names)
 from .config import (set_config, get_config, get_config_path, set_cache_dir,
                      set_memmap_min_size, get_subjects_dir, _get_stim_channel,
                      sys_info, _get_extra_data_path, _get_root_dir,


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
@@ -6,6 +6,7 @@
 from itertools import product
 import os
 import os.path as op
+import shutil
 from unittest import SkipTest
 
 import pytest
@@ -13,6 +14,7 @@
 from numpy.testing import (assert_array_almost_equal, assert_array_equal,
                            assert_allclose, assert_equal)
 from scipy import stats, linalg
+from scipy.io import loadmat, savemat
 import matplotlib.pyplot as plt
 
 from mne import (Epochs, read_events, pick_types, create_info, EpochsArray,
@@ -1311,6 +1313,33 @@ def test_read_ica_eeglab():
                     rtol=1e-05, atol=1e-08)
 
 
+@testing.requires_testing_data
+def test_read_ica_eeglab_mismatch(tmpdir):
+    """Test read_ica_eeglab function when there is a mismatch."""
+    fname_orig = op.join(test_base_dir, "EEGLAB", "test_raw.set")
+    base = op.basename(fname_orig)[:-3]
+    shutil.copyfile(fname_orig[:-3] + 'fdt', tmpdir.join(base + 'fdt'))
+    fname = tmpdir.join(base)
+    data = loadmat(fname_orig)
+    w = data['EEG']['icaweights'][0][0]
+    w[:] = np.random.RandomState(0).randn(*w.shape)
+    savemat(str(fname), data, appendmat=False)
+    assert op.isfile(fname)
+    with pytest.warns(RuntimeWarning, match='Mismatch.*removal.*icawinv.*'):
+        ica = read_ica_eeglab(fname)
+    _assert_ica_attributes(ica)
+    ica_correct = read_ica_eeglab(fname_orig)
+    attrs = [attr for attr in dir(ica_correct)
+             if attr.endswith('_') and not attr.startswith('_')]
+    assert 'mixing_matrix_' in attrs
+    assert 'unmixing_matrix_' in attrs
+    assert ica.labels_ == ica_correct.labels_ == {}
+    attrs.pop(attrs.index('labels_'))
+    for attr in attrs:
+        a, b = getattr(ica, attr), getattr(ica_correct, attr)
+        assert_allclose(a, b, rtol=1e-12, atol=1e-12, err_msg=attr)
+
+
 def _assert_ica_attributes(ica, data=None, limits=(1.0, 70)):
     """Assert some attributes of ICA objects."""
     __tracebackhide__ = True