From 94dedb3fc4b65a47af2ccab25942625d9be7ef60 Mon Sep 17 00:00:00 2001 From: Eric Larson Date: Fri, 12 Jun 2020 17:15:23 -0400 Subject: [PATCH 01/10] BUG: Fix bug with volume morph and subject_to!="fsaverage" --- doc/changes/latest.inc | 2 + examples/inverse/plot_morph_volume_stc.py | 13 ++- mne/datasets/utils.py | 4 +- mne/morph.py | 79 +++++++++++-------- mne/tests/test_morph.py | 59 ++++++++++++-- .../source-modeling/plot_beamformer_lcmv.py | 1 + 6 files changed, 110 insertions(+), 48 deletions(-) diff --git a/doc/changes/latest.inc b/doc/changes/latest.inc index cb7e526a0ff..d1cb444908d 100644 --- a/doc/changes/latest.inc +++ b/doc/changes/latest.inc @@ -112,6 +112,8 @@ Bug - Fix bug with :func:`mne.compute_source_morph` when more than one volume source space was present (e.g., when using labels) where only the first label would be interpolated when ``mri_resolution=True`` by `Eric Larson`_ +- Fix bug with :func:`mne.compute_source_morph` when morphing to a volume source space when ``src_to`` is used and the destination subject is not ``fsaverage`` by `Eric Larson`_ + - Fix bug with :func:`mne.minimum_norm.compute_source_psd_epochs` and :func:`mne.minimum_norm.source_band_induced_power` raised errors when ``method='eLORETA'`` by `Eric Larson`_ - Fix bug with :func:`mne.minimum_norm.apply_inverse` where the explained variance did not work for complex data by `Eric Larson`_ diff --git a/examples/inverse/plot_morph_volume_stc.py b/examples/inverse/plot_morph_volume_stc.py index a6935de3fc9..9995772d489 100644 --- a/examples/inverse/plot_morph_volume_stc.py +++ b/examples/inverse/plot_morph_volume_stc.py @@ -9,7 +9,8 @@ :class:`mne.VolSourceEstimate` to a common reference space. We achieve this using :class:`mne.SourceMorph`. Pre-computed data will be morphed based on an affine transformation and a nonlinear registration method -known as Symmetric Diffeomorphic Registration (SDR) by Avants et al. [1]_. +known as Symmetric Diffeomorphic Registration (SDR) by +:footcite:`AvantsEtAl2008`. Transformation is estimated from the subject's anatomical T1 weighted MRI (brain) to `FreeSurfer's 'fsaverage' T1 weighted MRI (brain) @@ -18,13 +19,6 @@ Afterwards the transformation will be applied to the volumetric source estimate. The result will be plotted, showing the fsaverage T1 weighted anatomical MRI, overlaid with the morphed volumetric source estimate. - -References ----------- -.. [1] Avants, B. B., Epstein, C. L., Grossman, M., & Gee, J. C. (2009). - Symmetric Diffeomorphic Image Registration with Cross- Correlation: - Evaluating Automated Labeling of Elderly and Neurodegenerative - Brain, 12(1), 26-41. """ # Author: Tommy Clausner # @@ -153,3 +147,6 @@ # # >>> morph.apply(stc) # +# References +# ---------- +# .. footbibligraphy:: diff --git a/mne/datasets/utils.py b/mne/datasets/utils.py index cfe5f58dd28..ebdb25300cb 100644 --- a/mne/datasets/utils.py +++ b/mne/datasets/utils.py @@ -239,7 +239,7 @@ def _data_path(path=None, force_update=False, update_path=True, download=True, path = _get_path(path, key, name) # To update the testing or misc dataset, push commits, then make a new # release on GitHub. Then update the "releases" variable: - releases = dict(testing='0.92', misc='0.6') + releases = dict(testing='0.93', misc='0.6') # And also update the "md5_hashes['testing']" variable below. # To update any other dataset, update the data archive itself (upload @@ -326,7 +326,7 @@ def _data_path(path=None, force_update=False, update_path=True, download=True, sample='12b75d1cb7df9dfb4ad73ed82f61094f', somato='ea825966c0a1e9b2f84e3826c5500161', spm='9f43f67150e3b694b523a21eb929ea75', - testing='42daafd1b882da2ef041de860ca6e771', + testing='2eb998a0893a28faedd583973c70b517', multimodal='26ec847ae9ab80f58f204d09e2c08367', fnirs_motor='c4935d19ddab35422a69f3326a01fef8', opm='370ad1dcfd5c47e029e692c85358a374', diff --git a/mne/morph.py b/mne/morph.py index 5ad9792852d..90028e51919 100644 --- a/mne/morph.py +++ b/mne/morph.py @@ -17,6 +17,7 @@ _BaseVolSourceEstimate, _BaseSourceEstimate, _get_ico_tris) from .source_space import SourceSpaces, _ensure_src from .surface import read_morph_map, mesh_edges, read_surface, _compute_nearest +from .transforms import _angle_between_quats, rot_to_quat from .utils import (logger, verbose, check_version, get_subjects_dir, warn as warn_, fill_doc, _check_option, _validate_type, BunchConst, wrapped_stdout, _check_fname, warn, @@ -33,8 +34,9 @@ def compute_source_morph(src, subject_from=None, subject_to='fsaverage', """Create a SourceMorph from one subject to another. Method is based on spherical morphing by FreeSurfer for surface - cortical estimates [1]_ and Symmetric Diffeomorphic Registration - for volumic data [2]_. + cortical estimates :footcite:`GreveEtAl2013` and + Symmetric Diffeomorphic Registration for volumic data + :footcite:`AvantsEtAl2008`. Parameters ---------- @@ -134,7 +136,7 @@ def compute_source_morph(src, subject_from=None, subject_to='fsaverage', obtained as described `here `_. For statistical comparisons between hemispheres, use of the symmetric ``fsaverage_sym`` - model is recommended to minimize bias [1]_. + model is recommended to minimize bias :footcite:`GreveEtAl2013`. .. versionadded:: 0.17.0 @@ -143,14 +145,7 @@ def compute_source_morph(src, subject_from=None, subject_to='fsaverage', References ---------- - .. [1] Greve D. N., Van der Haegen L., Cai Q., Stufflebeam S., Sabuncu M. - R., Fischl B., Brysbaert M. - A Surface-based Analysis of Language Lateralization and Cortical - Asymmetry. Journal of Cognitive Neuroscience 25(9), 1477-1492, 2013. - .. [2] Avants, B. B., Epstein, C. L., Grossman, M., & Gee, J. C. (2009). - Symmetric Diffeomorphic Image Registration with Cross- Correlation: - Evaluating Automated Labeling of Elderly and Neurodegenerative - Brain, 12(1), 26-41. + .. footbibliography:: """ src_data, kind, src_subject = _get_src_data(src) subject_from = _check_subject_src(subject_from, src_subject) @@ -179,13 +174,13 @@ def compute_source_morph(src, subject_from=None, subject_to='fsaverage', _check_dep(nibabel='2.1.0', dipy='0.10.1') import nibabel as nib - logger.info('volume source space(s) present...') + logger.info('Volume source space(s) present...') # load moving MRI mri_subpath = op.join('mri', 'brain.mgz') mri_path_from = op.join(subjects_dir, subject_from, mri_subpath) - logger.info('loading %s as "from" volume' % mri_path_from) + logger.info(' Loading %s as "from" volume' % mri_path_from) with warnings.catch_warnings(): mri_from = nib.load(mri_path_from) @@ -194,7 +189,7 @@ def compute_source_morph(src, subject_from=None, subject_to='fsaverage', mri_path_to = op.join(subjects_dir, subject_to, mri_subpath) if not op.isfile(mri_path_to): raise IOError('cannot read file: %s' % mri_path_to) - logger.info('loading %s as "to" volume' % mri_path_to) + logger.info(' Loading %s as "to" volume' % mri_path_to) with warnings.catch_warnings(): mri_to = nib.load(mri_path_to) @@ -206,11 +201,15 @@ def compute_source_morph(src, subject_from=None, subject_to='fsaverage', 'mixed source space') else: surf_offset = 2 if src_to.kind == 'mixed' else 0 - src_data['to_vox_map'] = ( - src_to[-1]['shape'], src_to[-1]['src_mri_t']['trans'] * - np.array([[1e3, 1e3, 1e3, 1]]).T) + # All of our computations are in RAS (like img.affine), so we need + # to get the transformation from RAS to the source space + # subsampling of vox (src), not MRI (FreeSurfer surface RAS) to src + src_ras_t = np.dot(src_to[-1]['mri_ras_t']['trans'], + src_to[-1]['src_mri_t']['trans']) + src_ras_t[:3] *= 1e3 + src_data['to_vox_map'] = (src_to[-1]['shape'], src_ras_t) vertices_to_vol = [s['vertno'] for s in src_to[surf_offset:]] - zooms_src_to = np.diag(src_data['to_vox_map'][1])[:3] + zooms_src_to = np.diag(src_to[-1]['src_mri_t']['trans'])[:3] * 1000 assert (zooms_src_to[0] == zooms_src_to).all() zooms_src_to = tuple(zooms_src_to) @@ -312,7 +311,7 @@ class SourceMorph(object): Number of levels (``len(niter_sdr)``) and number of iterations per level - for each successive stage of iterative refinement - to perform the Symmetric Diffeomorphic Registration (sdr) - transform [2]_. + transform :footcite:`AvantsEtAl2008`. spacing : int | list | None See :func:`mne.compute_source_morph`. smooth : int | str | None @@ -321,7 +320,7 @@ class SourceMorph(object): Morph across hemisphere. morph_mat : scipy.sparse.csr_matrix The sparse surface morphing matrix for spherical surface - based morphing [1]_. + based morphing :footcite:`GreveEtAl2013`. vertices_to : list of ndarray The destination surface vertices. shape : tuple @@ -344,14 +343,7 @@ class SourceMorph(object): References ---------- - .. [1] Greve D. N., Van der Haegen L., Cai Q., Stufflebeam S., Sabuncu M. - R., Fischl B., Brysbaert M. - A Surface-based Analysis of Language Lateralization and Cortical - Asymmetry. Journal of Cognitive Neuroscience 25(9), 1477-1492, 2013. - .. [2] Avants, B. B., Epstein, C. L., Grossman, M., & Gee, J. C. (2009). - Symmetric Diffeomorphic Image Registration with Cross- Correlation: - Evaluating Automated Labeling of Elderly and Neurodegenerative - Brain, 12(1), 26-41. + .. footbibliography:: """ def __init__(self, subject_from, subject_to, kind, zooms, @@ -481,9 +473,9 @@ def _morph_one_vol(self, one): # subselect the correct cube if src_to is provided if self.src_data['to_vox_map'] is not None: # order=0 (nearest) should be fine since it's just subselecting - img_to = _get_img_fdata(resample_from_to( - SpatialImage(img_to, self.affine), - self.src_data['to_vox_map'], order=0)) + img_to = SpatialImage(img_to, self.affine) + img_to = resample_from_to(img_to, self.src_data['to_vox_map'], 0) + img_to = _get_img_fdata(img_to) # reshape to nvoxel x nvol: # in the MNE definition of volume source spaces, @@ -889,7 +881,7 @@ def _compute_morph_sdr(mri_from, mri_to, niter_affine, niter_sdr, zooms): mri_to = nib.Nifti1Image(mri_to_res, mri_to_res_affine) affine = mri_to.affine - mri_to = _get_img_fdata(mri_to) # to ndarray + mri_to = _get_img_fdata(mri_to).copy() # to ndarray mri_to /= mri_to.max() mri_from_affine = mri_from.affine # get mri_from to world transform mri_from = _get_img_fdata(mri_from) # to ndarray @@ -919,6 +911,13 @@ def _compute_morph_sdr(mri_from, mri_to, niter_affine, niter_sdr, zooms): rigid = affreg.optimize( mri_to, mri_from, transforms.RigidTransform3D(), None, affine, mri_from_affine, starting_affine=translation.affine) + dist = np.linalg.norm(rigid.affine[:3, 3]) + angle = np.rad2deg(_angle_between_quats( + np.zeros(3), rot_to_quat(rigid.affine[:3, :3]))) + + logger.info(f'Translation: {dist:5.1f} mm') + logger.info(f'Rotation: {angle:5.1f}°') + logger.info('') # affine transform (translation + rotation + scaling) logger.info('Optimizing full affine:') @@ -934,6 +933,22 @@ def _compute_morph_sdr(mri_from, mri_to, niter_affine, niter_sdr, zooms): with wrapped_stdout(indent=' '): sdr_morph = sdr.optimize(mri_to, pre_affine.transform(mri_from)) shape = tuple(sdr_morph.domain_shape) # should be tuple of int + + mri_from_to = sdr_morph.transform(pre_affine.transform(mri_from)) + mri_to, mri_from_to = mri_to.ravel(), mri_from_to.ravel() + mri_from_to /= np.linalg.norm(mri_from_to) + mri_to /= np.linalg.norm(mri_to) + r2 = 100 * (mri_to @ mri_from_to) + logger.info(f'Variance explained by morph: {r2:0.1f}%') + + # To debug to_vox_map, this can be used: + # from nibabel.processing import resample_from_to + # mri_from_to = sdr_morph.transform(pre_affine.transform(mri_from)) + # mri_from_to = nib.Nifti1Image(mri_from_to, affine) + # fig1 = mri_from_to.orthoview() + # mri_from_to_cut = resample_from_to(mri_from_to, to_vox_map, 1) + # fig2 = mri_from_to_cut.orthoview() + return shape, zooms, affine, pre_affine, sdr_morph diff --git a/mne/tests/test_morph.py b/mne/tests/test_morph.py index 0497d2f781b..4c1e8c693e2 100644 --- a/mne/tests/test_morph.py +++ b/mne/tests/test_morph.py @@ -23,7 +23,7 @@ from mne.minimum_norm import (apply_inverse, read_inverse_operator, make_inverse_operator) from mne.source_space import (get_volume_labels_from_aseg, _get_mri_info_data, - _get_atlas_values) + _get_atlas_values, _add_interpolator) from mne.utils import (run_tests_if_main, requires_nibabel, check_version, requires_dipy, requires_h5py) from mne.fixes import _get_args @@ -39,16 +39,19 @@ 'sample_audvis_trunc-meg-vol-7-meg-inv.fif') fname_fwd_vol = op.join(sample_dir, 'sample_audvis_trunc-meg-vol-7-fwd.fif') -fname_vol = op.join(sample_dir, - 'sample_audvis_trunc-grad-vol-7-fwd-sensmap-vol.w') +fname_vol_w = op.join(sample_dir, + 'sample_audvis_trunc-grad-vol-7-fwd-sensmap-vol.w') fname_inv_surf = op.join(sample_dir, 'sample_audvis_trunc-meg-eeg-oct-6-meg-inv.fif') fname_fmorph = op.join(data_path, 'MEG', 'sample', 'fsaverage_audvis_trunc-meg') fname_smorph = op.join(sample_dir, 'sample_audvis_trunc-meg') fname_t1 = op.join(subjects_dir, 'sample', 'mri', 'T1.mgz') +fname_vol = op.join(subjects_dir, 'sample', 'bem', 'sample-volume-7mm-src.fif') fname_brain = op.join(subjects_dir, 'sample', 'mri', 'brain.mgz') fname_aseg = op.join(subjects_dir, 'sample', 'mri', 'aseg.mgz') +fname_fs_vol = op.join(subjects_dir, 'fsaverage', 'bem', + 'fsaverage-vol7-nointerp-src.fif.gz') fname_aseg_fs = op.join(subjects_dir, 'fsaverage', 'mri', 'aseg.mgz') fname_stc = op.join(sample_dir, 'fsaverage_audvis_trunc-meg') @@ -245,7 +248,7 @@ def test_surface_vector_source_morph(tmpdir): assert isinstance(source_morph_surf.apply(stc_surf), SourceEstimate) # degenerate - stc_vol = read_source_estimate(fname_vol, 'sample') + stc_vol = read_source_estimate(fname_vol_w, 'sample') with pytest.raises(TypeError, match='stc_from must be an instance'): source_morph_surf.apply(stc_vol) @@ -259,7 +262,7 @@ def test_volume_source_morph(tmpdir): """Test volume source estimate morph, special cases and exceptions.""" import nibabel as nib inverse_operator_vol = read_inverse_operator(fname_inv_vol) - stc_vol = read_source_estimate(fname_vol, 'sample') + stc_vol = read_source_estimate(fname_vol_w, 'sample') # check for invalid input type with pytest.raises(TypeError, match='src must be'): @@ -284,7 +287,7 @@ def test_volume_source_morph(tmpdir): with pytest.raises(ValueError, match='Only surface.*sparse morph'): compute_source_morph(src=src, sparse=True, subjects_dir=subjects_dir) - # terrible quality buts fast + # terrible quality but fast zooms = 20 kwargs = dict(zooms=zooms, niter_sdr=(1,), niter_affine=(1,)) source_morph_vol = compute_source_morph( @@ -433,6 +436,50 @@ def test_volume_source_morph(tmpdir): source_morph_vol.apply(stc_vol_bad) +@requires_h5py +@requires_nibabel() +@requires_dipy() +@pytest.mark.slowtest +@testing.requires_testing_data +def test_volume_source_morph_round_trip(tmpdir): + """Test volume source estimate morph round-trips well.""" + src_sample = mne.read_source_spaces(fname_vol) + src_sample[0]['subject_his_id'] = 'sample' + assert src_sample[0]['nuse'] == 4157 + use = np.linspace(0, src_sample[0]['nuse'] - 1, 10).round().astype(int) + # Created to save space with: + # + # bem = op.join(op.dirname(mne.__file__), 'data', 'fsaverage', + # 'fsaverage-inner_skull-bem.fif') + # src_fsaverage = mne.setup_volume_source_space( + # 'fsaverage', pos=7., bem=bem, mindist=0, subjects_dir=subjects_dir, + # add_interpolator=False) + # mne.write_source_spaces(fname_fs_vol, src_fsaverage, overwrite=True) + # + # For speed we do it without the interpolator because it's huge. + src_fsaverage = mne.read_source_spaces(fname_fs_vol) + src_fsaverage[0].update(vol_dims=np.array([23, 29, 25]), seg_name='brain') + _add_interpolator(src_fsaverage, True) + assert src_fsaverage[0]['nuse'] == 6379 + kwargs = dict(niter_sdr=(2, 1, 1), niter_affine=(1,), + subjects_dir=subjects_dir) + sample_to_fs = compute_source_morph( + src=src_sample, src_to=src_fsaverage, subject_to='fsaverage', + **kwargs) + fs_to_sample = compute_source_morph( + src=src_fsaverage, src_to=src_sample, subject_to='sample', + **kwargs) + stc_sample = VolSourceEstimate( + np.eye(src_sample[0]['nuse'])[:, use], [src_sample[0]['vertno']], 0, 1) + stc_fsaverage = sample_to_fs.apply(stc_sample) + stc_sample_rt = fs_to_sample.apply(stc_fsaverage) + maxs = np.argmax(stc_sample_rt.data, axis=0) + src_rr = src_sample[0]['rr'][src_sample[0]['vertno']] + dists = 1000 * np.linalg.norm(src_rr[use] - src_rr[maxs], axis=1) + mu = np.mean(dists) + assert 7 < mu < 9 # 7.97; 25.4 without the src_ras_t fix + + @pytest.mark.slowtest @testing.requires_testing_data def test_morph_stc_dense(): diff --git a/tutorials/source-modeling/plot_beamformer_lcmv.py b/tutorials/source-modeling/plot_beamformer_lcmv.py index 6b3ad76578f..6ce86b0208b 100644 --- a/tutorials/source-modeling/plot_beamformer_lcmv.py +++ b/tutorials/source-modeling/plot_beamformer_lcmv.py @@ -9,6 +9,7 @@ .. contents:: Page contents :local: :depth: 2 + """ # Author: Britta Westner # From 31cfdae3bdebd7100b82f137e835ef1fd0b4480b Mon Sep 17 00:00:00 2001 From: Eric Larson Date: Fri, 12 Jun 2020 19:01:35 -0400 Subject: [PATCH 02/10] FIX: Spelling, expose round-trip problem --- doc/changes/latest.inc | 2 + examples/inverse/plot_morph_volume_stc.py | 2 +- mne/morph.py | 21 ++++--- mne/tests/test_morph.py | 73 +++++++++++++---------- 4 files changed, 59 insertions(+), 39 deletions(-) diff --git a/doc/changes/latest.inc b/doc/changes/latest.inc index d1cb444908d..bc18523bf17 100644 --- a/doc/changes/latest.inc +++ b/doc/changes/latest.inc @@ -33,6 +33,8 @@ Changelog - Add support for mixed source spaces to :func:`mne.compute_source_morph` by `Eric Larson`_ +- Add support for omitting the SDR step in volumetric morphing by passing ``n_iter_sdr=()`` to `mne.compute_source_morph` by `Eric Larson`_ + - Add support for providing the destination surface source space in the ``src_to`` argument of :func:`mne.compute_source_morph` by `Eric Larson`_ - Add ``tol_kind`` option to :func:`mne.compute_rank` by `Eric Larson`_ diff --git a/examples/inverse/plot_morph_volume_stc.py b/examples/inverse/plot_morph_volume_stc.py index 9995772d489..4b7581ab007 100644 --- a/examples/inverse/plot_morph_volume_stc.py +++ b/examples/inverse/plot_morph_volume_stc.py @@ -149,4 +149,4 @@ # # References # ---------- -# .. footbibligraphy:: +# .. footbibliography:: diff --git a/mne/morph.py b/mne/morph.py index 90028e51919..2968012f5ad 100644 --- a/mne/morph.py +++ b/mne/morph.py @@ -468,7 +468,9 @@ def _morph_one_vol(self, one): img_to, self.affine, _get_zooms_orig(self), self.zooms) # morph data - img_to = self.sdr_morph.transform(self.pre_affine.transform(img_to)) + img_to = self.pre_affine.transform(img_to) + if self.sdr_morph is not None: + img_to = self.sdr_morph.transform(img_to) # subselect the correct cube if src_to is provided if self.src_data['to_vox_map'] is not None: @@ -927,14 +929,17 @@ def _compute_morph_sdr(mri_from, mri_to, niter_affine, niter_sdr, zooms): affine, mri_from_affine, starting_affine=rigid.affine) # compute mapping - sdr = imwarp.SymmetricDiffeomorphicRegistration( - metrics.CCMetric(3), list(niter_sdr)) - logger.info('Optimizing SDR:') - with wrapped_stdout(indent=' '): - sdr_morph = sdr.optimize(mri_to, pre_affine.transform(mri_from)) - shape = tuple(sdr_morph.domain_shape) # should be tuple of int + mri_from_to = pre_affine.transform(mri_from) + shape = tuple(pre_affine.domain_shape) + if len(niter_sdr): + sdr = imwarp.SymmetricDiffeomorphicRegistration( + metrics.CCMetric(3), list(niter_sdr)) + logger.info('Optimizing SDR:') + with wrapped_stdout(indent=' '): + sdr_morph = sdr.optimize(mri_to, pre_affine.transform(mri_from)) + assert shape == tuple(sdr_morph.domain_shape) # should be tuple of int + mri_from_to = sdr_morph.transform(mri_from_to) - mri_from_to = sdr_morph.transform(pre_affine.transform(mri_from)) mri_to, mri_from_to = mri_to.ravel(), mri_from_to.ravel() mri_from_to /= np.linalg.norm(mri_from_to) mri_to /= np.linalg.norm(mri_to) diff --git a/mne/tests/test_morph.py b/mne/tests/test_morph.py index 4c1e8c693e2..1abc84d5a7c 100644 --- a/mne/tests/test_morph.py +++ b/mne/tests/test_morph.py @@ -441,43 +441,56 @@ def test_volume_source_morph(tmpdir): @requires_dipy() @pytest.mark.slowtest @testing.requires_testing_data -def test_volume_source_morph_round_trip(tmpdir): +@pytest.mark.parametrize('subject_from, subject_to, lower, upper', [ + ('sample', 'fsaverage', 7, 9), + ('fsaverage', 'fsaverage', 23, 25), # indicative of a bug + ('sample', 'sample', 8, 10), # indicative of a bug +]) +def test_volume_source_morph_round_trip( + tmpdir, subject_from, subject_to, lower, upper): """Test volume source estimate morph round-trips well.""" - src_sample = mne.read_source_spaces(fname_vol) - src_sample[0]['subject_his_id'] = 'sample' - assert src_sample[0]['nuse'] == 4157 - use = np.linspace(0, src_sample[0]['nuse'] - 1, 10).round().astype(int) - # Created to save space with: - # - # bem = op.join(op.dirname(mne.__file__), 'data', 'fsaverage', - # 'fsaverage-inner_skull-bem.fif') - # src_fsaverage = mne.setup_volume_source_space( - # 'fsaverage', pos=7., bem=bem, mindist=0, subjects_dir=subjects_dir, - # add_interpolator=False) - # mne.write_source_spaces(fname_fs_vol, src_fsaverage, overwrite=True) - # - # For speed we do it without the interpolator because it's huge. - src_fsaverage = mne.read_source_spaces(fname_fs_vol) - src_fsaverage[0].update(vol_dims=np.array([23, 29, 25]), seg_name='brain') - _add_interpolator(src_fsaverage, True) - assert src_fsaverage[0]['nuse'] == 6379 + src = dict() + if 'sample' in (subject_from, subject_to): + src['sample'] = mne.read_source_spaces(fname_vol) + src['sample'][0]['subject_his_id'] = 'sample' + assert src['sample'][0]['nuse'] == 4157 + if 'fsaverage' in (subject_from, subject_to): + # Created to save space with: + # + # bem = op.join(op.dirname(mne.__file__), 'data', 'fsaverage', + # 'fsaverage-inner_skull-bem.fif') + # src_fsaverage = mne.setup_volume_source_space( + # 'fsaverage', pos=7., bem=bem, mindist=0, subjects_dir=subjects_dir, + # add_interpolator=False) + # mne.write_source_spaces(fname_fs_vol, src_fsaverage, overwrite=True) + # + # For speed we do it without the interpolator because it's huge. + src['fsaverage'] = mne.read_source_spaces(fname_fs_vol) + src['fsaverage'][0].update( + vol_dims=np.array([23, 29, 25]), seg_name='brain') + _add_interpolator(src['fsaverage'], True) + assert src['fsaverage'][0]['nuse'] == 6379 + src_to, src_from = src[subject_to], src[subject_from] + del src + # XXX: Change to no SDR for speed once everything works kwargs = dict(niter_sdr=(2, 1, 1), niter_affine=(1,), subjects_dir=subjects_dir) - sample_to_fs = compute_source_morph( - src=src_sample, src_to=src_fsaverage, subject_to='fsaverage', + morph_from_to = compute_source_morph( + src=src_from, src_to=src_to, subject_to=subject_to, **kwargs) - fs_to_sample = compute_source_morph( - src=src_fsaverage, src_to=src_sample, subject_to='sample', + morph_to_from = compute_source_morph( + src=src_to, src_to=src_from, subject_to=subject_from, **kwargs) - stc_sample = VolSourceEstimate( - np.eye(src_sample[0]['nuse'])[:, use], [src_sample[0]['vertno']], 0, 1) - stc_fsaverage = sample_to_fs.apply(stc_sample) - stc_sample_rt = fs_to_sample.apply(stc_fsaverage) - maxs = np.argmax(stc_sample_rt.data, axis=0) - src_rr = src_sample[0]['rr'][src_sample[0]['vertno']] + use = np.linspace(0, src_from[0]['nuse'] - 1, 10).round().astype(int) + stc_from = VolSourceEstimate( + np.eye(src_from[0]['nuse'])[:, use], [src_from[0]['vertno']], 0, 1) + stc_to = morph_from_to.apply(stc_from) + stc_from_rt = morph_to_from.apply(stc_to) + maxs = np.argmax(stc_from_rt.data, axis=0) + src_rr = src_from[0]['rr'][src_from[0]['vertno']] dists = 1000 * np.linalg.norm(src_rr[use] - src_rr[maxs], axis=1) mu = np.mean(dists) - assert 7 < mu < 9 # 7.97; 25.4 without the src_ras_t fix + assert lower <= mu < upper # fsaverage=7.97; 25.4 without src_ras_t fix @pytest.mark.slowtest From 0307237e9e3662111a4afe1e9ca6cd27ca25b289 Mon Sep 17 00:00:00 2001 From: Eric Larson Date: Fri, 12 Jun 2020 19:02:17 -0400 Subject: [PATCH 03/10] STY: PEP8 --- mne/tests/test_morph.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mne/tests/test_morph.py b/mne/tests/test_morph.py index 1abc84d5a7c..b4310c9c238 100644 --- a/mne/tests/test_morph.py +++ b/mne/tests/test_morph.py @@ -460,8 +460,8 @@ def test_volume_source_morph_round_trip( # bem = op.join(op.dirname(mne.__file__), 'data', 'fsaverage', # 'fsaverage-inner_skull-bem.fif') # src_fsaverage = mne.setup_volume_source_space( - # 'fsaverage', pos=7., bem=bem, mindist=0, subjects_dir=subjects_dir, - # add_interpolator=False) + # 'fsaverage', pos=7., bem=bem, mindist=0, + # subjects_dir=subjects_dir, add_interpolator=False) # mne.write_source_spaces(fname_fs_vol, src_fsaverage, overwrite=True) # # For speed we do it without the interpolator because it's huge. From 7379e7be04c0be0c38da6ed5142f1093d5429c6a Mon Sep 17 00:00:00 2001 From: Eric Larson Date: Fri, 12 Jun 2020 19:02:55 -0400 Subject: [PATCH 04/10] STY: Shorter --- mne/tests/test_morph.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/mne/tests/test_morph.py b/mne/tests/test_morph.py index b4310c9c238..8170da31188 100644 --- a/mne/tests/test_morph.py +++ b/mne/tests/test_morph.py @@ -476,11 +476,9 @@ def test_volume_source_morph_round_trip( kwargs = dict(niter_sdr=(2, 1, 1), niter_affine=(1,), subjects_dir=subjects_dir) morph_from_to = compute_source_morph( - src=src_from, src_to=src_to, subject_to=subject_to, - **kwargs) + src=src_from, src_to=src_to, subject_to=subject_to, **kwargs) morph_to_from = compute_source_morph( - src=src_to, src_to=src_from, subject_to=subject_from, - **kwargs) + src=src_to, src_to=src_from, subject_to=subject_from, **kwargs) use = np.linspace(0, src_from[0]['nuse'] - 1, 10).round().astype(int) stc_from = VolSourceEstimate( np.eye(src_from[0]['nuse'])[:, use], [src_from[0]['vertno']], 0, 1) From 41d05e91a0cd9ce3175fa2c57cafc9f848bd2b49 Mon Sep 17 00:00:00 2001 From: Eric Larson Date: Fri, 12 Jun 2020 20:09:39 -0400 Subject: [PATCH 05/10] MAINT: Expose another issue --- mne/morph.py | 2 ++ mne/tests/test_morph.py | 12 ++++++++++++ 2 files changed, 14 insertions(+) diff --git a/mne/morph.py b/mne/morph.py index 2968012f5ad..dbfa517ff82 100644 --- a/mne/morph.py +++ b/mne/morph.py @@ -939,6 +939,8 @@ def _compute_morph_sdr(mri_from, mri_to, niter_affine, niter_sdr, zooms): sdr_morph = sdr.optimize(mri_to, pre_affine.transform(mri_from)) assert shape == tuple(sdr_morph.domain_shape) # should be tuple of int mri_from_to = sdr_morph.transform(mri_from_to) + else: + sdr_morph = None mri_to, mri_from_to = mri_to.ravel(), mri_from_to.ravel() mri_from_to /= np.linalg.norm(mri_from_to) diff --git a/mne/tests/test_morph.py b/mne/tests/test_morph.py index 8170da31188..34f31db4f9b 100644 --- a/mne/tests/test_morph.py +++ b/mne/tests/test_morph.py @@ -199,6 +199,16 @@ def test_surface_source_morph_round_trip(smooth, lower, upper, n_warn): stc_back = morph_back.apply(stc_fs) corr = np.corrcoef(stc.data.ravel(), stc_back.data.ravel())[0, 1] assert lower <= corr <= upper + # check the round-trip power + assert_power_preserved(stc, stc_back) + + +def assert_power_preserved(orig, new, limits=(1., 1.05)): + """Assert that the power is preserved during a round-trip morph.""" + __tracebackhide__ = True + power_ratio = np.linalg.norm(orig.data) / np.linalg.norm(new.data) + min_, max_ = limits + assert min_ < power_ratio < max_, 'Power ratio' @requires_h5py @@ -489,6 +499,8 @@ def test_volume_source_morph_round_trip( dists = 1000 * np.linalg.norm(src_rr[use] - src_rr[maxs], axis=1) mu = np.mean(dists) assert lower <= mu < upper # fsaverage=7.97; 25.4 without src_ras_t fix + # XXX The surface version has limits (1, 1.05), which is reasonable... + assert_power_preserved(stc_from, stc_from_rt, limits=(7, 8)) @pytest.mark.slowtest From c95daf9a6a693662d029cd8385a9f21c5d6dd202 Mon Sep 17 00:00:00 2001 From: Eric Larson Date: Fri, 12 Jun 2020 20:13:11 -0400 Subject: [PATCH 06/10] DOC: More ideas [ci skip] --- mne/tests/test_morph.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/mne/tests/test_morph.py b/mne/tests/test_morph.py index 34f31db4f9b..103f6a765f4 100644 --- a/mne/tests/test_morph.py +++ b/mne/tests/test_morph.py @@ -501,6 +501,11 @@ def test_volume_source_morph_round_trip( assert lower <= mu < upper # fsaverage=7.97; 25.4 without src_ras_t fix # XXX The surface version has limits (1, 1.05), which is reasonable... assert_power_preserved(stc_from, stc_from_rt, limits=(7, 8)) + # XXX need to add: before and after morph, check the proportion of vertices + # that are in the brainmask.mgz, and the proportion of brainmask vertices + # that are occupied (these should not change a lot) + # XXX check that the `pre_affine` is close to identity when + # subject_from==subject_to @pytest.mark.slowtest From b122a57966108c6dd19efe910df063c2c67b52ff Mon Sep 17 00:00:00 2001 From: Eric Larson Date: Sat, 13 Jun 2020 09:30:18 -0400 Subject: [PATCH 07/10] MAINT: Better test --- mne/tests/test_morph.py | 49 ++++++++++++++++++++++++++++------------- 1 file changed, 34 insertions(+), 15 deletions(-) diff --git a/mne/tests/test_morph.py b/mne/tests/test_morph.py index 103f6a765f4..f68df67e3a2 100644 --- a/mne/tests/test_morph.py +++ b/mne/tests/test_morph.py @@ -452,13 +452,15 @@ def test_volume_source_morph(tmpdir): @pytest.mark.slowtest @testing.requires_testing_data @pytest.mark.parametrize('subject_from, subject_to, lower, upper', [ - ('sample', 'fsaverage', 7, 9), - ('fsaverage', 'fsaverage', 23, 25), # indicative of a bug - ('sample', 'sample', 8, 10), # indicative of a bug + ('sample', 'fsaverage', 13.5, 14.5), + ('fsaverage', 'fsaverage', 11.5, 12.5), + ('sample', 'sample', 10, 11), ]) def test_volume_source_morph_round_trip( tmpdir, subject_from, subject_to, lower, upper): """Test volume source estimate morph round-trips well.""" + import nibabel as nib + from nibabel.processing import resample_from_to src = dict() if 'sample' in (subject_from, subject_to): src['sample'] = mne.read_source_spaces(fname_vol) @@ -482,9 +484,9 @@ def test_volume_source_morph_round_trip( assert src['fsaverage'][0]['nuse'] == 6379 src_to, src_from = src[subject_to], src[subject_from] del src - # XXX: Change to no SDR for speed once everything works - kwargs = dict(niter_sdr=(2, 1, 1), niter_affine=(1,), - subjects_dir=subjects_dir) + # No SDR just for speed once everything works + kwargs = dict(niter_sdr=(), niter_affine=(1,), + subjects_dir=subjects_dir, verbose=True) morph_from_to = compute_source_morph( src=src_from, src_to=src_to, subject_to=subject_to, **kwargs) morph_to_from = compute_source_morph( @@ -492,20 +494,37 @@ def test_volume_source_morph_round_trip( use = np.linspace(0, src_from[0]['nuse'] - 1, 10).round().astype(int) stc_from = VolSourceEstimate( np.eye(src_from[0]['nuse'])[:, use], [src_from[0]['vertno']], 0, 1) - stc_to = morph_from_to.apply(stc_from) - stc_from_rt = morph_to_from.apply(stc_to) + stc_from_rt = morph_to_from.apply(morph_from_to.apply(stc_from)) maxs = np.argmax(stc_from_rt.data, axis=0) src_rr = src_from[0]['rr'][src_from[0]['vertno']] dists = 1000 * np.linalg.norm(src_rr[use] - src_rr[maxs], axis=1) mu = np.mean(dists) assert lower <= mu < upper # fsaverage=7.97; 25.4 without src_ras_t fix - # XXX The surface version has limits (1, 1.05), which is reasonable... - assert_power_preserved(stc_from, stc_from_rt, limits=(7, 8)) - # XXX need to add: before and after morph, check the proportion of vertices - # that are in the brainmask.mgz, and the proportion of brainmask vertices - # that are occupied (these should not change a lot) - # XXX check that the `pre_affine` is close to identity when - # subject_from==subject_to + # check that pre_affine is close to identity when subject_to==subject_from + if subject_to == subject_from: + for morph in (morph_to_from, morph_from_to): + assert_allclose( + morph.pre_affine.affine, np.eye(4), atol=1e-2) + # check that power is more or less preserved + ratio = stc_from.data.size / stc_from_rt.data.size + limits = ratio * np.array([1, 1.2]) + stc_from.crop(0, 0)._data.fill(1.) + stc_from_rt = morph_to_from.apply(morph_from_to.apply(stc_from)) + assert_power_preserved(stc_from, stc_from_rt, limits=limits) + # before and after morph, check the proportion of vertices + # that are inside and outside the brainmask.mgz + brain = nib.load(op.join(subjects_dir, subject_from, 'mri', 'brain.mgz')) + mask = _get_img_fdata(brain) > 0 + if subject_from == subject_to == 'sample': + for stc in [stc_from, stc_from_rt]: + img = stc.as_volume(src_from, mri_resolution=True) + img = nib.Nifti1Image(_get_img_fdata(img)[:, :, :, 0], img.affine) + img = _get_img_fdata(resample_from_to(img, brain, order=1)) + assert img.shape == mask.shape + in_ = img[mask].astype(bool).mean() + out = img[~mask].astype(bool).mean() + assert 0.97 < in_ < 0.98 + assert out < 0.02 @pytest.mark.slowtest From 1fd861059d8307bfe9d6b94319853c764e083256 Mon Sep 17 00:00:00 2001 From: Eric Larson Date: Mon, 15 Jun 2020 12:03:30 -0400 Subject: [PATCH 08/10] FIX: First order interp --- doc/changes/latest.inc | 4 ++++ mne/morph.py | 2 +- mne/source_estimate.py | 2 +- mne/tests/test_morph.py | 6 +++--- mne/viz/_3d.py | 30 ++++++++++++++++++++---------- mne/viz/tests/test_3d.py | 19 ++++++++++++------- 6 files changed, 41 insertions(+), 22 deletions(-) diff --git a/doc/changes/latest.inc b/doc/changes/latest.inc index bc18523bf17..8097f318b61 100644 --- a/doc/changes/latest.inc +++ b/doc/changes/latest.inc @@ -35,6 +35,8 @@ Changelog - Add support for omitting the SDR step in volumetric morphing by passing ``n_iter_sdr=()`` to `mne.compute_source_morph` by `Eric Larson`_ +- Add support for passing a string argument to ``bg_img`` in `mne.viz.plot_volume_source_stimates` by `Eric Larson`_ + - Add support for providing the destination surface source space in the ``src_to`` argument of :func:`mne.compute_source_morph` by `Eric Larson`_ - Add ``tol_kind`` option to :func:`mne.compute_rank` by `Eric Larson`_ @@ -116,6 +118,8 @@ Bug - Fix bug with :func:`mne.compute_source_morph` when morphing to a volume source space when ``src_to`` is used and the destination subject is not ``fsaverage`` by `Eric Larson`_ +- Fix bug with :func:`mne.compute_source_morph` where outermost voxels in the destination source space could be errantly omitted by `Eric Larson`_ + - Fix bug with :func:`mne.minimum_norm.compute_source_psd_epochs` and :func:`mne.minimum_norm.source_band_induced_power` raised errors when ``method='eLORETA'`` by `Eric Larson`_ - Fix bug with :func:`mne.minimum_norm.apply_inverse` where the explained variance did not work for complex data by `Eric Larson`_ diff --git a/mne/morph.py b/mne/morph.py index dbfa517ff82..7d6e557adea 100644 --- a/mne/morph.py +++ b/mne/morph.py @@ -476,7 +476,7 @@ def _morph_one_vol(self, one): if self.src_data['to_vox_map'] is not None: # order=0 (nearest) should be fine since it's just subselecting img_to = SpatialImage(img_to, self.affine) - img_to = resample_from_to(img_to, self.src_data['to_vox_map'], 0) + img_to = resample_from_to(img_to, self.src_data['to_vox_map'], 1) img_to = _get_img_fdata(img_to) # reshape to nvoxel x nvol: diff --git a/mne/source_estimate.py b/mne/source_estimate.py index b852444891d..470af032846 100644 --- a/mne/source_estimate.py +++ b/mne/source_estimate.py @@ -1822,7 +1822,7 @@ class _BaseVolSourceEstimate(_BaseSourceEstimate): @copy_function_doc_to_method_doc(plot_volume_source_estimates) def plot(self, src, subject=None, subjects_dir=None, mode='stat_map', - bg_img=None, colorbar=True, colormap='auto', clim='auto', + bg_img='T1.mgz', colorbar=True, colormap='auto', clim='auto', transparent='auto', show=True, initial_time=None, initial_pos=None, verbose=None): data = self.magnitude() if self._data_ndim == 3 else self diff --git a/mne/tests/test_morph.py b/mne/tests/test_morph.py index f68df67e3a2..6ba0785746f 100644 --- a/mne/tests/test_morph.py +++ b/mne/tests/test_morph.py @@ -452,9 +452,9 @@ def test_volume_source_morph(tmpdir): @pytest.mark.slowtest @testing.requires_testing_data @pytest.mark.parametrize('subject_from, subject_to, lower, upper', [ - ('sample', 'fsaverage', 13.5, 14.5), - ('fsaverage', 'fsaverage', 11.5, 12.5), - ('sample', 'sample', 10, 11), + ('sample', 'fsaverage', 8.5, 9), + ('fsaverage', 'fsaverage', 7, 7.5), + ('sample', 'sample', 6, 7), ]) def test_volume_source_morph_round_trip( tmpdir, subject_from, subject_to, lower, upper): diff --git a/mne/viz/_3d.py b/mne/viz/_3d.py index feac812fb40..f4b5c286a65 100644 --- a/mne/viz/_3d.py +++ b/mne/viz/_3d.py @@ -39,9 +39,10 @@ read_ras_mni_t, _print_coord_trans) from ..utils import (get_subjects_dir, logger, _check_subject, verbose, warn, has_nibabel, check_version, fill_doc, _pl, get_config, - _ensure_int, _validate_type, _check_option) + _ensure_int, _validate_type, _check_option, _check_fname) from .utils import (mne_analyze_colormap, _get_color_list, plt_show, tight_layout, figure_nobar, _check_time_unit) +from .misc import _check_mri from ..bem import (ConductorModel, _bem_find_surface, _surf_dict, _surf_name, read_bem_surfaces) @@ -1811,10 +1812,20 @@ def _ijk_to_cut_coords(ijk, img): return apply_trans(img.affine, ijk) +def _load_subject_mri(mri, stc, subject, subjects_dir, name): + import nibabel as nib + from nibabel.spatialimages import SpatialImage + _validate_type(mri, ('path-like', SpatialImage), name) + if isinstance(mri, str): + subject = _check_subject(stc.subject, subject, True) + mri = nib.load(_check_mri(mri, subject, subjects_dir)) + return mri + + @verbose def plot_volume_source_estimates(stc, src, subject=None, subjects_dir=None, - mode='stat_map', bg_img=None, colorbar=True, - colormap='auto', clim='auto', + mode='stat_map', bg_img='T1.mgz', + colorbar=True, colormap='auto', clim='auto', transparent=None, show=True, initial_time=None, initial_pos=None, verbose=None): @@ -1839,9 +1850,10 @@ def plot_volume_source_estimates(stc, src, subject=None, subjects_dir=None, The plotting mode to use. Either 'stat_map' (default) or 'glass_brain'. For "glass_brain", activation absolute values are displayed after being transformed to a standard MNI brain. - bg_img : instance of SpatialImage | None + bg_img : instance of SpatialImage | str The background image used in the nilearn plotting function. - If None, it is the T1.mgz file that is found in the subjects_dir. + Can also be a string to use the ``bg_img`` file in the subject's + MRI directory (default is ``'T1.mgz'``). Not used in "glass brain" plotting. colorbar : bool, optional If True, display a colorbar on the right of the plots. @@ -2071,11 +2083,9 @@ def _onclick(event, params, verbose=None): bg_img = None # not used else: # stat_map if bg_img is None: - subject = _check_subject(stc.subject, subject, True) - subjects_dir = get_subjects_dir(subjects_dir=subjects_dir, - raise_error=True) - t1_fname = op.join(subjects_dir, subject, 'mri', 'T1.mgz') - bg_img = nib.load(t1_fname) + bg_img = 'T1.mgz' + bg_img = _load_subject_mri( + bg_img, stc, subject, subjects_dir, 'bg_img') if initial_time is None: time_sl = slice(0, None) diff --git a/mne/viz/tests/test_3d.py b/mne/viz/tests/test_3d.py index e1a4977c960..23167069e1f 100644 --- a/mne/viz/tests/test_3d.py +++ b/mne/viz/tests/test_3d.py @@ -605,13 +605,14 @@ def test_snapshot_brain_montage(renderer): @requires_dipy() @requires_nibabel() @requires_version('nilearn', '0.4') -@pytest.mark.parametrize('mode, stype, init_t, want_t, init_p, want_p', [ - ('glass_brain', 's', None, 2, None, (-30.9, 18.4, 56.7)), - ('stat_map', 'vec', 1, 1, None, (15.7, 16.0, -6.3)), - ('glass_brain', 'vec', None, 1, (10, -10, 20), (6.6, -9.0, 19.9)), - ('stat_map', 's', 1, 1, (-10, 5, 10), (-12.3, 2.0, 7.7))]) +@pytest.mark.parametrize( + 'mode, stype, init_t, want_t, init_p, want_p, bg_img', [ + ('glass_brain', 's', None, 2, None, (-30.9, 18.4, 56.7), None), + ('stat_map', 'vec', 1, 1, None, (15.7, 16.0, -6.3), None), + ('glass_brain', 'vec', None, 1, (10, -10, 20), (6.6, -9., 19.9), None), + ('stat_map', 's', 1, 1, (-10, 5, 10), (-12.3, 2.0, 7.7), 'brain.mgz')]) def test_plot_volume_source_estimates(mode, stype, init_t, want_t, - init_p, want_p): + init_p, want_p, bg_img): """Test interactive plotting of volume source estimates.""" forward = read_forward_solution(fwd_fname) sample_src = forward['src'] @@ -634,7 +635,7 @@ def test_plot_volume_source_estimates(mode, stype, init_t, want_t, fig = stc.plot( sample_src, subject='sample', subjects_dir=subjects_dir, mode=mode, initial_time=init_t, initial_pos=init_p, - verbose=True) + bg_img=bg_img, verbose=True) log = log.getvalue() want_str = 't = %0.3f s' % want_t assert want_str in log, (want_str, init_t) @@ -644,6 +645,10 @@ def test_plot_volume_source_estimates(mode, stype, init_t, want_t, _fake_click(fig, fig.axes[ax_idx], (0.3, 0.5)) fig.canvas.key_press_event('left') fig.canvas.key_press_event('shift+right') + if bg_img is not None: + with pytest.raises(FileNotFoundError, match='MRI file .* not found'): + stc.plot(sample_src, subject='sample', subjects_dir=subjects_dir, + mode='stat_map', bg_img='junk.mgz') @pytest.mark.slowtest # can be slow on OSX From dbf8a53870ef51f7a38adb4473af44245fb6a2b3 Mon Sep 17 00:00:00 2001 From: Eric Larson Date: Mon, 15 Jun 2020 12:38:56 -0400 Subject: [PATCH 09/10] STY: Flake --- mne/viz/_3d.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mne/viz/_3d.py b/mne/viz/_3d.py index f4b5c286a65..16b5b5c6115 100644 --- a/mne/viz/_3d.py +++ b/mne/viz/_3d.py @@ -39,7 +39,7 @@ read_ras_mni_t, _print_coord_trans) from ..utils import (get_subjects_dir, logger, _check_subject, verbose, warn, has_nibabel, check_version, fill_doc, _pl, get_config, - _ensure_int, _validate_type, _check_option, _check_fname) + _ensure_int, _validate_type, _check_option) from .utils import (mne_analyze_colormap, _get_color_list, plt_show, tight_layout, figure_nobar, _check_time_unit) from .misc import _check_mri From 39daf0b423bcfd7906796b9ad9802322967290b6 Mon Sep 17 00:00:00 2001 From: Eric Larson Date: Mon, 15 Jun 2020 13:06:05 -0400 Subject: [PATCH 10/10] STY: Spelling --- doc/changes/latest.inc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/changes/latest.inc b/doc/changes/latest.inc index 8097f318b61..1fffa53956a 100644 --- a/doc/changes/latest.inc +++ b/doc/changes/latest.inc @@ -35,7 +35,7 @@ Changelog - Add support for omitting the SDR step in volumetric morphing by passing ``n_iter_sdr=()`` to `mne.compute_source_morph` by `Eric Larson`_ -- Add support for passing a string argument to ``bg_img`` in `mne.viz.plot_volume_source_stimates` by `Eric Larson`_ +- Add support for passing a string argument to ``bg_img`` in `mne.viz.plot_volume_source_estimates` by `Eric Larson`_ - Add support for providing the destination surface source space in the ``src_to`` argument of :func:`mne.compute_source_morph` by `Eric Larson`_