Repository: mne-tools/mne-python

Issue:
Skipping dense skin surface topology check
For reasons I don't really understand nor can clearly spot in MRI data the high-density skin surface routine `make_scalp_surfaces` fails b/c the srf topology is botched and causes `_assert_complete_surface` to throw...  
>RuntimeError: Surface outer skin is not complete (sum of solid angles = 6.2829 * 4*PI instead).

I've tried `--force` and cutting off the neck but still hitting this more frequnetly than I'd like.
So I thought a PR to skip the skin surface topology check might be useful?


Discussion:
what code are you referring to?

this command
https://mne-tools.github.io/dev/generated/commands.html#mne-make-scalp-surfaces
?

where is the error raised?

Yes. To be more specific e.g.
>mne make_scalp_surfaces --overwrite --subject subj -n -f

raises...

> 
> Traceback (most recent call last):
>   File ".../mne-python/mne/commands/mne_make_scalp_surfaces.py", line 130, in <module>
>     run()
>   File ".../mne-python/mne/commands/mne_make_scalp_surfaces.py", line 61, in run
>     options.no_decimate, options.verbose)
>   File "<string>", line 2, in _run
>   File ".../mne-python/mne/utils.py", line 728, in verbose
>     return function(*args, **kwargs)
>   File ".../mne-python/mne/commands/mne_make_scalp_surfaces.py", line 107, in _run
>     [surf], [mne.io.constants.FIFF.FIFFV_BEM_SURF_ID_HEAD], [1])[0]
>   File ".../mne-python/mne/bem.py", line 496, in _surfaces_to_bem
>     _check_surfaces(surfs)
>   File ".../mne-python/mne/bem.py", line 433, in _check_surfaces
>     _assert_complete_surface(surf)
>   File ".../mne-python/mne/bem.py", line 410, in _assert_complete_surface
>     (_surf_name[surf['id']], tot_angle))
> RuntimeError: Surface outer skin  is not complete (sum of solid angles = 6.2829 * 4*PI instead).
> 
yes we should be less strict. Can you change the python code to force the
non-crash?

>Can you change the python code to force the non-crash?

That is the intent. Initial thinking was to ignore/skip `_assert_complete_surface` for the skin srf.  But you're right in that we should reduce the passing criterion. Right now the error is raised if `np.abs(tot_angle / (2 * np.pi) - 1.0) > 1e-5`. Where did `1e-5` come from? and what would be less strict? 
if you use force option then make it a warning not an error


Resolving patch:
diff --git a/mne/bem.py b/mne/bem.py
--- a/mne/bem.py
+++ b/mne/bem.py
@@ -394,7 +394,7 @@ def _order_surfaces(surfs):
     return surfs
 
 
-def _assert_complete_surface(surf):
+def _assert_complete_surface(surf, incomplete='raise'):
     """Check the sum of solid angles as seen from inside."""
     # from surface_checks.c
     tot_angle = 0.
@@ -404,10 +404,15 @@ def _assert_complete_surface(surf):
                 (_surf_name[surf['id']],
                  1000 * cm[0], 1000 * cm[1], 1000 * cm[2]))
     tot_angle = _get_solids(surf['rr'][surf['tris']], cm[np.newaxis, :])[0]
-    if np.abs(tot_angle / (2 * np.pi) - 1.0) > 1e-5:
-        raise RuntimeError('Surface %s is not complete (sum of solid angles '
-                           '= %g * 4*PI instead).' %
-                           (_surf_name[surf['id']], tot_angle))
+    prop = tot_angle / (2 * np.pi)
+    if np.abs(prop - 1.0) > 1e-5:
+        msg = ('Surface %s is not complete (sum of solid angles '
+               'yielded %g, should be 1.)'
+               % (_surf_name[surf['id']], prop))
+        if incomplete == 'raise':
+            raise RuntimeError(msg)
+        else:
+            warn(msg)
 
 
 _surf_name = {
@@ -427,10 +432,10 @@ def _assert_inside(fro, to):
                            % (_surf_name[fro['id']], _surf_name[to['id']]))
 
 
-def _check_surfaces(surfs):
+def _check_surfaces(surfs, incomplete='raise'):
     """Check that the surfaces are complete and non-intersecting."""
     for surf in surfs:
-        _assert_complete_surface(surf)
+        _assert_complete_surface(surf, incomplete=incomplete)
     # Then check the topology
     for surf_1, surf_2 in zip(surfs[:-1], surfs[1:]):
         logger.info('Checking that %s surface is inside %s surface...' %
@@ -462,7 +467,8 @@ def _check_thicknesses(surfs):
                      1000 * min_dist))
 
 
-def _surfaces_to_bem(surfs, ids, sigmas, ico=None, rescale=True):
+def _surfaces_to_bem(surfs, ids, sigmas, ico=None, rescale=True,
+                     incomplete='raise'):
     """Convert surfaces to a BEM."""
     # equivalent of mne_surf2bem
     # surfs can be strings (filenames) or surface dicts
@@ -493,7 +499,7 @@ def _surfaces_to_bem(surfs, ids, sigmas, ico=None, rescale=True):
     surfs = _order_surfaces(surfs)
 
     # Check topology as best we can
-    _check_surfaces(surfs)
+    _check_surfaces(surfs, incomplete=incomplete)
     for surf in surfs:
         _check_surface_size(surf)
     _check_thicknesses(surfs)
diff --git a/mne/commands/mne_make_scalp_surfaces.py b/mne/commands/mne_make_scalp_surfaces.py
--- a/mne/commands/mne_make_scalp_surfaces.py
+++ b/mne/commands/mne_make_scalp_surfaces.py
@@ -56,7 +56,6 @@ def run():
     if subject is None or subjects_dir is None:
         parser.print_help()
         sys.exit(1)
-    print(options.no_decimate)
     _run(subjects_dir, subject, options.force, options.overwrite,
          options.no_decimate, options.verbose)
 
@@ -70,7 +69,7 @@ def _run(subjects_dir, subject, force, overwrite, no_decimate, verbose=None):
     if 'FREESURFER_HOME' not in this_env:
         raise RuntimeError('The FreeSurfer environment needs to be set up '
                            'for this script')
-    force = '--force' if force else '--check'
+    incomplete = 'warn' if force else 'raise'
     subj_path = op.join(subjects_dir, subject)
     if not op.exists(subj_path):
         raise RuntimeError('%s does not exist. Please check your subject '
@@ -104,7 +103,8 @@ def check_seghead(surf_path=op.join(subj_path, 'surf')):
     logger.info('2. Creating %s ...' % dense_fname)
     _check_file(dense_fname, overwrite)
     surf = mne.bem._surfaces_to_bem(
-        [surf], [mne.io.constants.FIFF.FIFFV_BEM_SURF_ID_HEAD], [1])[0]
+        [surf], [mne.io.constants.FIFF.FIFFV_BEM_SURF_ID_HEAD], [1],
+        incomplete=incomplete)[0]
     mne.write_bem_surfaces(dense_fname, surf)
     levels = 'medium', 'sparse'
     tris = [] if no_decimate else [30000, 2500]
@@ -122,9 +122,10 @@ def check_seghead(surf_path=op.join(subj_path, 'surf')):
         _check_file(dec_fname, overwrite)
         dec_surf = mne.bem._surfaces_to_bem(
             [dict(rr=points, tris=tris)],
-            [mne.io.constants.FIFF.FIFFV_BEM_SURF_ID_HEAD], [1], rescale=False)
+            [mne.io.constants.FIFF.FIFFV_BEM_SURF_ID_HEAD], [1], rescale=False,
+            incomplete=incomplete)
         mne.write_bem_surfaces(dec_fname, dec_surf)
 
-is_main = (__name__ == '__main__')
-if is_main:
+
+if (__name__ == '__main__'):
     run()


Test patch: