Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

MRG, ENH: Better error messages for ICA #7879

Merged
merged 1 commit into from
Jun 8, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions doc/changes/latest.inc
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,8 @@ Changelog

- Add "on_missing='raise'" to :meth:`mne.io.Raw.set_montage` and related functions to allow ignoring of missing electrode coordinates by `Adam Li`_

- Add better sanity checking of ``max_pca_components`` and ``n_components`` to provide more informative error messages for :class:`mne.preprocessing.ICA` by `Eric Larson`_

- Add ``plot`` option to :meth:`mne.viz.plot_filter` allowing selection of which filter properties are plotted and added option for user to supply ``axes`` by `Robert Luke`_

- Add estimation method legend to :func:`mne.viz.plot_snr_estimate` by `Eric Larson`_
Expand Down
82 changes: 36 additions & 46 deletions mne/preprocessing/ica.py
Original file line number Diff line number Diff line change
Expand Up @@ -483,11 +483,39 @@ def fit(self, inst, picks=None, start=None, stop=None, decim=None,
_check_for_unsupported_ica_channels(
picks, inst.info, allow_ref_meg=self.allow_ref_meg)

# Actually start fitting
t_start = time()
if self.current_fit != 'unfitted':
self._reset()

logger.info('Fitting ICA to data using %i channels '
'(please be patient, this may take a while)' % len(picks))

if self.max_pca_components is None:
self.max_pca_components = len(picks)
logger.info('Inferring max_pca_components from picks')
elif self.max_pca_components > len(picks):
raise ValueError(
f'ica.max_pca_components ({self.max_pca_components}) cannot '
f'be greater than len(picks) ({len(picks)})')
# n_components could be float 0 < x <= 1, but that's okay here
if self.n_components is not None and self.n_components > len(picks):
raise ValueError(
f'ica.n_components ({self.n_components}) cannot '
f'be greater than len(picks) ({len(picks)})')

# filter out all the channels the raw wouldn't have initialized
self.info = pick_info(inst.info, picks)

if self.info['comps']:
self.info['comps'] = []
self.ch_names = self.info['ch_names']

if isinstance(inst, BaseRaw):
self._fit_raw(inst, picks, start, stop, decim, reject, flat,
tstep, reject_by_annotation, verbose)
elif isinstance(inst, BaseEpochs):
else:
assert isinstance(inst, BaseEpochs)
self._fit_epochs(inst, picks, decim, verbose)

# sort ICA components by explained variance
Expand All @@ -500,37 +528,16 @@ def fit(self, inst, picks=None, start=None, stop=None, decim=None,

def _reset(self):
"""Aux method."""
del self.pre_whitener_
del self.unmixing_matrix_
del self.mixing_matrix_
del self.n_components_
del self.n_samples_
del self.pca_components_
del self.pca_explained_variance_
del self.pca_mean_
del self.n_iter_
if hasattr(self, 'drop_inds_'):
del self.drop_inds_
if hasattr(self, 'reject_'):
del self.reject_
for key in ('pre_whitener_', 'unmixing_matrix_', 'mixing_matrix_',
'n_components_', 'n_samples_', 'pca_components_',
'pca_explained_variance_', 'pca_mean_', 'n_iter_',
'drop_inds_', 'reject_'):
if hasattr(self, key):
delattr(self, key)

def _fit_raw(self, raw, picks, start, stop, decim, reject, flat, tstep,
reject_by_annotation, verbose):
"""Aux method."""
if self.current_fit != 'unfitted':
self._reset()

logger.info('Fitting ICA to data using %i channels '
'(please be patient, this may take a while)' % len(picks))

if self.max_pca_components is None:
self.max_pca_components = len(picks)
logger.info('Inferring max_pca_components from picks')

self.info = pick_info(raw.info, picks)
if self.info['comps']:
self.info['comps'] = []
self.ch_names = self.info['ch_names']
start, stop = _check_start_stop(raw, start, stop)

reject_by_annotation = 'omit' if reject_by_annotation else None
Expand Down Expand Up @@ -558,28 +565,11 @@ def _fit_raw(self, raw, picks, start, stop, decim, reject, flat, tstep,

def _fit_epochs(self, epochs, picks, decim, verbose):
"""Aux method."""
if self.current_fit != 'unfitted':
self._reset()

if epochs.events.size == 0:
raise RuntimeError('Tried to fit ICA with epochs, but none were '
'found: epochs.events is "{}".'
.format(epochs.events))

logger.info('Fitting ICA to data using %i channels '
'(please be patient, this may take a while)' % len(picks))

# filter out all the channels the raw wouldn't have initialized
self.info = pick_info(epochs.info, picks)

if self.info['comps']:
self.info['comps'] = []
self.ch_names = self.info['ch_names']

if self.max_pca_components is None:
self.max_pca_components = len(picks)
logger.info('Inferring max_pca_components from picks')

# this should be a copy (picks a list of int)
data = epochs.get_data()[:, picks]
# this will be a view
Expand Down Expand Up @@ -1018,7 +1008,7 @@ def score_sources(self, inst, target=None, score_func='pearsonr',
reject_by_annotation)

if sources.shape[-1] != target.shape[-1]:
raise ValueError('Sources and target do not have the same'
raise ValueError('Sources and target do not have the same '
'number of time slices.')
# auto target selection
if isinstance(inst, BaseRaw):
Expand Down
48 changes: 34 additions & 14 deletions mne/preprocessing/tests/test_ica.py
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,8 @@ def test_ica_full_data_recovery(method):
else:
diff = np.abs(evoked.data[:n_channels] - data2)
assert (np.max(diff) > 1e-14)
pytest.raises(ValueError, ICA, method='pizza-decomposision')
with pytest.raises(ValueError, match='Invalid value'):
ICA(method='pizza-decomposision')


@pytest.mark.parametrize("method", ["fastica", "picard"])
Expand Down Expand Up @@ -264,22 +265,27 @@ def test_ica_core(method):
picks_, methods)

# # test init catchers
pytest.raises(ValueError, ICA, n_components=3, max_pca_components=2)
pytest.raises(ValueError, ICA, n_components=2.3, max_pca_components=2)
with pytest.raises(ValueError, match='must be smaller than max_pca'):
ICA(n_components=3, max_pca_components=2)
with pytest.raises(ValueError, match='explained variance needs values'):
ICA(n_components=2.3, max_pca_components=3)

# test essential core functionality
for n_cov, n_comp, max_n, pcks, method in iter_ica_params:
# Test ICA raw
ica = ICA(noise_cov=n_cov, n_components=n_comp,
max_pca_components=max_n, n_pca_components=max_n,
random_state=0, method=method, max_iter=1)
pytest.raises(ValueError, ica.__contains__, 'mag')
with pytest.raises(ValueError, match='Cannot check for channels of t'):
'meg' in ica

print(ica) # to test repr

# test fit checker
pytest.raises(RuntimeError, ica.get_sources, raw)
pytest.raises(RuntimeError, ica.get_sources, epochs)
with pytest.raises(RuntimeError, match='No fit available'):
ica.get_sources(raw)
with pytest.raises(RuntimeError, match='No fit available'):
ica.get_sources(epochs)

# Test error upon empty epochs fitting
with pytest.raises(RuntimeError, match='none were found'):
Expand Down Expand Up @@ -336,14 +342,14 @@ def test_ica_core(method):
sources = ica.get_sources(epochs).get_data()
assert (sources.shape[1] == ica.n_components_)

pytest.raises(ValueError, ica.score_sources, epochs,
target=np.arange(1))
with pytest.raises(ValueError, match='target do not have the same nu'):
ica.score_sources(epochs, target=np.arange(1))

# test preload filter
epochs3 = epochs.copy()
epochs3.preload = False
pytest.raises(RuntimeError, ica.apply, epochs3,
include=[1, 2])
with pytest.raises(RuntimeError, match='requires epochs data to be l'):
ica.apply(epochs3, include=[1, 2])

# test for bug with whitener updating
_pre_whitener = ica.pre_whitener_.copy()
Expand All @@ -353,12 +359,26 @@ def test_ica_core(method):

# test expl. var threshold leading to empty sel
ica.n_components = 0.1
pytest.raises(RuntimeError, ica.fit, epochs)
with pytest.raises(RuntimeError, match='One PCA component captures most'):
ica.fit(epochs)

offender = 1, 2, 3,
pytest.raises(ValueError, ica.get_sources, offender)
pytest.raises(TypeError, ica.fit, offender)
pytest.raises(TypeError, ica.apply, offender)
with pytest.raises(ValueError, match='Data input must be of Raw'):
ica.get_sources(offender)
with pytest.raises(TypeError, match='must be an instance of'):
ica.fit(offender)
with pytest.raises(TypeError, match='must be an instance of'):
ica.apply(offender)

# gh-7868
ica.max_pca_components = 3
ica.n_components = 0.99
with pytest.raises(ValueError, match='pca_components.*cannot be greater'):
ica.fit(epochs, picks=[0, 1])
ica.max_pca_components = None
ica.n_components = 3
with pytest.raises(ValueError, match='n_components.*cannot be greater'):
ica.fit(epochs, picks=[0, 1])


@requires_sklearn
Expand Down