Repository: mne-tools/mne-python

Issue:
bem meshes generated by brainsuite cannot be downsampled (mne.make_bem_model))
Hi guys,
I'm having some issues with the BEM meshes for my EEG data.  I've tried two approaches:
(1) Using freesurfer watershed and trying different preflood levels, but it seems to do a bad job with the outer_skull.surf being misplaced (it extends down to the neck, mostly following the outer_skin.surf).  Will this be problematic for source localization?  Is there a way to overcome this?

![src_surf_bem](https://user-images.githubusercontent.com/49346757/55673263-b56fd800-5873-11e9-9f8a-bf11bae23305.png)



(2a) I've had better success with using Brainsuite and have followed the guidelines (https://martinos.org/mne/stable/manual/appendix/bem_model.html); converting it from dfs to surf using mne_convert_surface, and then reducing the number of triangles to 10,000 using mne_reduce_surface.  When I try to run `mne.make_bem_model`, I get the error:

> File "/usr/anaconda3/envs/mne/lib/python3.6/site-packages/mne/bem.py", line 344, in _ico_downsample
> raise RuntimeError(bad_msg)
> RuntimeError: A surface with 10000 triangles cannot be isomorphic with a subdivided icosahedron.

I can overcome this issue by setting 'ico=None' but am not sure if this affects source localization.  I've attached the .surf I generated using Brainsuite on the MNE sample dataset if anyone wanted to test it.

MNE sample dataset:
[inner_skull.surf.gz](https://github.com/mne-tools/mne-python/files/3050758/inner_skull.surf.gz)
[outer_skin.surf.gz](https://github.com/mne-tools/mne-python/files/3050759/outer_skin.surf.gz)
[outer_skull.surf.gz](https://github.com/mne-tools/mne-python/files/3050760/outer_skull.surf.gz)


(2b) When I set the 'ico=None', I run into a new issue with mne.make_bem_solution after mne.make_bem_model.  I don't get this issue with the MNE sample dataset but with my acquired T1's.  

My acquired T1's:
[inner_skull.surf.gz](https://github.com/mne-tools/mne-python/files/3054720/inner_skull.surf.gz)
[outer_skin.surf.gz](https://github.com/mne-tools/mne-python/files/3054721/outer_skin.surf.gz)
[outer_skull.surf.gz](https://github.com/mne-tools/mne-python/files/3054722/outer_skull.surf.gz)

`bem = mne.make_bem_solution(surfaces, verbose='INFO') `

> Approximation method : Linear collocation
> 
> Three-layer model surfaces loaded.
> Computing the linear collocation solution...
> Matrix coefficients...
> head (5002) -> head (5002) ...
> /dagher/dagher6/yyau/toolbox/anaconda3/envs/mne/lib/python3.6/site-packages/mne/bem.py:151: RuntimeWarning: divide by zero encountered in double_scalars
> miss /= (4.0 * n_memb)
> head (5002) -> outer_skull (5002) ...
> head (5002) -> inner_skull (5002) ...
> outer_skull (5002) -> head (5002) ...
> outer_skull (5002) -> outer_skull (5002) ...
> outer_skull (5002) -> inner_skull (5002) ...
> inner_skull (5002) -> head (5002) ...
> inner_skull (5002) -> outer_skull (5002) ...
> inner_skull (5002) -> inner_skull (5002) ...
> Inverting the coefficient matrix...
> Traceback (most recent call last):
> 
> File "<ipython-input-18-24943e2f3a65>", line 1, in <module>
> bem = mne.make_bem_solution(surfaces, verbose='INFO')
> 
> File "</dagher/dagher6/yyau/toolbox/anaconda3/envs/mne/lib/python3.6/site-packages/mne/externals/decorator.py:decorator-gen-39>", line 2, in make_bem_solution
> 
> File "/dagher/dagher6/yyau/toolbox/anaconda3/envs/mne/lib/python3.6/site-packages/mne/utils/_logging.py", line 88, in wrapper
> return function(*args, **kwargs)
> 
> File "/dagher/dagher6/yyau/toolbox/anaconda3/envs/mne/lib/python3.6/site-packages/mne/bem.py", line 326, in make_bem_solution
> _fwd_bem_linear_collocation_solution(bem)
> 
> File "/dagher/dagher6/yyau/toolbox/anaconda3/envs/mne/lib/python3.6/site-packages/mne/bem.py", line 267, in _fwd_bem_linear_collocation_solution
> m['solution'] = _fwd_bem_multi_solution(coeff, m['gamma'], nps)
> 
> File "/dagher/dagher6/yyau/toolbox/anaconda3/envs/mne/lib/python3.6/site-packages/mne/bem.py", line 225, in _fwd_bem_multi_solution
> return linalg.inv(solids, overwrite_a=True)
> 
> File "/dagher/dagher6/yyau/toolbox/anaconda3/envs/mne/lib/python3.6/site-packages/scipy/linalg/basic.py", line 945, in inv
> a1 = _asarray_validated(a, check_finite=check_finite)
> 
> File "/dagher/dagher6/yyau/toolbox/anaconda3/envs/mne/lib/python3.6/site-packages/scipy/_lib/_util.py", line 239, in _asarray_validated
> a = toarray(a)
> 
> File "/dagher/dagher6/yyau/toolbox/anaconda3/envs/mne/lib/python3.6/site-packages/numpy/lib/function_base.py", line 498, in asarray_chkfinite
> "array must not contain infs or NaNs")
> 
> ValueError: array must not contain infs or NaNs
> 

Any ideas?  Thanks for your help guys!


Discussion:
I cannot reproduce the issue you reported on the listserv with the `sample` surfaces. I put the unzipped surfaces in `foo/bem` and ran:
```
import numpy as np
import mne
bem = mne.make_bem_model('foo', None, subjects_dir='.', verbose=True)
with np.errstate(invalid='raise', divide='raise'):
    sol = mne.make_bem_solution(bem, verbose=True)
```
I just noticed that you uploaded the `sample` dateset surfaces. If we are going to debug the error you got (with the divide by zero), we'll need the actual problematic surfaces.
... I see you've now edited it include the surfaces (GitHub just updated the page for me -- we are not notified of edits.) I'll take a look now.

Resolving patch:
diff --git a/mne/bem.py b/mne/bem.py
--- a/mne/bem.py
+++ b/mne/bem.py
@@ -145,6 +145,7 @@ def _correct_auto_elements(surf, mat):
     for j, miss in enumerate(misses):
         # How much is missing?
         n_memb = len(surf['neighbor_tri'][j])
+        assert n_memb > 0  # should be guaranteed by our surface checks
         # The node itself receives one half
         mat[j, j] = miss / 2.0
         # The rest is divided evenly among the member nodes...
@@ -171,8 +172,8 @@ def _fwd_bem_lin_pot_coeff(surfs):
         rr_ord = np.arange(nps[si_1])
         for si_2, surf2 in enumerate(surfs):
             logger.info("        %s (%d) -> %s (%d) ..." %
-                        (_bem_explain_surface(surf1['id']), nps[si_1],
-                         _bem_explain_surface(surf2['id']), nps[si_2]))
+                        (_surf_name[surf1['id']], nps[si_1],
+                         _surf_name[surf2['id']], nps[si_2]))
             tri_rr = surf2['rr'][surf2['tris']]
             tri_nn = surf2['tri_nn']
             tri_area = surf2['tri_area']
@@ -252,11 +253,24 @@ def _fwd_bem_ip_modify_solution(solution, ip_solution, ip_mult, n_tri):
     return
 
 
+def _check_complete_surface(surf, copy=False):
+    surf = complete_surface_info(surf, copy=copy, verbose=False)
+    fewer = np.where([len(t) < 3 for t in surf['neighbor_tri']])[0]
+    if len(fewer) > 0:
+        raise RuntimeError('Surface %s has topological defects: %d / %d '
+                           'vertices have fewer than three neighboring '
+                           'triangles [%s]'
+                           % (_surf_name[surf['id']],
+                              len(fewer), surf['ntri'],
+                              ', '.join(str(f) for f in fewer)))
+    return surf
+
+
 def _fwd_bem_linear_collocation_solution(m):
     """Compute the linear collocation potential solution."""
     # first, add surface geometries
     for surf in m['surfs']:
-        complete_surface_info(surf, copy=False, verbose=False)
+        _check_complete_surface(surf)
 
     logger.info('Computing the linear collocation solution...')
     logger.info('    Matrix coefficients...')
@@ -483,7 +497,9 @@ def _surfaces_to_bem(surfs, ids, sigmas, ico=None, rescale=True,
         for si, surf in enumerate(surfs):
             surfs[si] = _ico_downsample(surf, ico)
     for surf, id_ in zip(surfs, ids):
+        # Do topology checks (but don't save data) to fail early
         surf['id'] = id_
+        _check_complete_surface(surf, copy=True)
         surf['coord_frame'] = surf.get('coord_frame', FIFF.FIFFV_COORD_MRI)
         surf.update(np=len(surf['rr']), ntri=len(surf['tris']))
         if rescale:
@@ -1233,7 +1249,7 @@ def read_bem_surfaces(fname, patch_stats=False, s_id=None, verbose=None):
             logger.info('    %d BEM surfaces read' % len(surf))
         for this in surf:
             if patch_stats or this['nn'] is None:
-                complete_surface_info(this, copy=False)
+                _check_complete_surface(this)
     return surf[0] if s_id is not None else surf
 
 
@@ -1424,7 +1440,7 @@ def _bem_find_surface(bem, id_):
         name = id_
         id_ = _surf_dict[id_]
     else:
-        name = _bem_explain_surface(id_)
+        name = _surf_name[id_]
     idx = np.where(np.array([s['id'] for s in bem['surfs']]) == id_)[0]
     if len(idx) != 1:
         raise RuntimeError('BEM model does not have the %s triangulation'
@@ -1432,12 +1448,6 @@ def _bem_find_surface(bem, id_):
     return bem['surfs'][idx[0]]
 
 
-def _bem_explain_surface(id_):
-    """Return a string corresponding to the given surface ID."""
-    _rev_dict = {val: key for key, val in _surf_dict.items()}
-    return _rev_dict[id_]
-
-
 # ############################################################################
 # Write
 
diff --git a/mne/dipole.py b/mne/dipole.py
--- a/mne/dipole.py
+++ b/mne/dipole.py
@@ -27,7 +27,7 @@
                                        _prep_field_computation)
 
 from .surface import transform_surface_to, _compute_nearest
-from .bem import _bem_find_surface, _bem_explain_surface
+from .bem import _bem_find_surface, _surf_name
 from .source_space import (_make_volume_source_space, SourceSpaces,
                            _points_outside_surface)
 from .parallel import parallel_func
@@ -630,7 +630,7 @@ def _make_guesses(surf, grid, exclude, mindist, n_jobs):
     """Make a guess space inside a sphere or BEM surface."""
     if 'rr' in surf:
         logger.info('Guess surface (%s) is in %s coordinates'
-                    % (_bem_explain_surface(surf['id']),
+                    % (_surf_name[surf['id']],
                        _coord_frame_name(surf['coord_frame'])))
     else:
         logger.info('Making a spherical guess space with radius %7.1f mm...'
diff --git a/mne/surface.py b/mne/surface.py
--- a/mne/surface.py
+++ b/mne/surface.py
@@ -356,16 +356,21 @@ def complete_surface_info(surf, do_neighbor_vert=False, copy=True,
     _normalize_vectors(surf['nn'])
 
     #   Check for topological defects
-    idx = np.where([len(n) == 0 for n in surf['neighbor_tri']])[0]
-    if len(idx) > 0:
-        logger.info('    Vertices [%s] do not have any neighboring'
-                    'triangles!' % ','.join([str(ii) for ii in idx]))
-    idx = np.where([len(n) < 3 for n in surf['neighbor_tri']])[0]
-    if len(idx) > 0:
-        logger.info('    Vertices [%s] have fewer than three neighboring '
-                    'tris, omitted' % ','.join([str(ii) for ii in idx]))
-        for k in idx:
-            surf['neighbor_tri'][k] = np.array([], int)
+    zero, fewer = list(), list()
+    for ni, n in enumerate(surf['neighbor_tri']):
+        if len(n) < 3:
+            if len(n) == 0:
+                zero.append(ni)
+            else:
+                fewer.append(ni)
+                surf['neighbor_tri'][ni] = np.array([], int)
+    if len(zero) > 0:
+        logger.info('    Vertices do not have any neighboring '
+                    'triangles: [%s]' % ', '.join(str(z) for z in zero))
+    if len(fewer) > 0:
+        logger.info('    Vertices have fewer than three neighboring '
+                    'triangles, removing neighbors: [%s]'
+                    % ', '.join(str(f) for f in fewer))
 
     #   Determine the neighboring vertices and fix errors
     if do_neighbor_vert is True:


Test patch:
diff --git a/mne/tests/test_bem.py b/mne/tests/test_bem.py
--- a/mne/tests/test_bem.py
+++ b/mne/tests/test_bem.py
@@ -3,7 +3,7 @@
 # License: BSD 3 clause
 
 from copy import deepcopy
-from os import remove
+from os import remove, makedirs
 import os.path as op
 from shutil import copy
 
@@ -14,7 +14,7 @@
 
 from mne import (make_bem_model, read_bem_surfaces, write_bem_surfaces,
                  make_bem_solution, read_bem_solution, write_bem_solution,
-                 make_sphere_model, Transform, Info)
+                 make_sphere_model, Transform, Info, write_surface)
 from mne.preprocessing.maxfilter import fit_sphere_to_headshape
 from mne.io.constants import FIFF
 from mne.transforms import translation
@@ -121,9 +121,9 @@ def test_make_sphere_model():
 
 
 @testing.requires_testing_data
-def test_bem_model():
+def test_make_bem_model(tmpdir):
     """Test BEM model creation from Python with I/O."""
-    tempdir = _TempDir()
+    tempdir = str(tmpdir)
     fname_temp = op.join(tempdir, 'temp-bem.fif')
     for kwargs, fname in zip((dict(), dict(conductivity=[0.3])),
                              [fname_bem_3, fname_bem_1]):
@@ -135,8 +135,32 @@ def test_bem_model():
         model_read = read_bem_surfaces(fname_temp)
         _compare_bem_surfaces(model, model_c)
         _compare_bem_surfaces(model_read, model_c)
-    pytest.raises(ValueError, make_bem_model, 'sample',  # bad conductivity
-                  conductivity=[0.3, 0.006], subjects_dir=subjects_dir)
+    # bad conductivity
+    with pytest.raises(ValueError, match='conductivity must be'):
+        make_bem_model('sample', 4, [0.3, 0.006], subjects_dir=subjects_dir)
+
+
+@testing.requires_testing_data
+def test_bem_model_topology(tmpdir):
+    """Test BEM model topological checks."""
+    # bad topology (not enough neighboring tris)
+    tempdir = str(tmpdir)
+    makedirs(op.join(tempdir, 'foo', 'bem'))
+    for fname in ('inner_skull', 'outer_skull', 'outer_skin'):
+        fname += '.surf'
+        copy(op.join(subjects_dir, 'sample', 'bem', fname),
+             op.join(tempdir, 'foo', 'bem', fname))
+    outer_fname = op.join(tempdir, 'foo', 'bem', 'outer_skull.surf')
+    rr, tris = read_surface(outer_fname)
+    tris = tris[:-1]
+    write_surface(outer_fname, rr, tris[:-1])
+    with pytest.raises(RuntimeError, match='Surface outer skull is not compl'):
+        make_bem_model('foo', None, subjects_dir=tempdir)
+    # Now get past this error to reach gh-6127 (not enough neighbor tris)
+    rr_bad = np.concatenate([rr, np.mean(rr, axis=0, keepdims=True)], axis=0)
+    write_surface(outer_fname, rr_bad, tris)
+    with pytest.raises(RuntimeError, match='Surface outer skull.*triangles'):
+        make_bem_model('foo', None, subjects_dir=tempdir)
 
 
 @pytest.mark.slowtest