Repository: mne-tools/mne-python

Issue:
BUG Writing forward obtained with make_forward_dipole
```
import pathlib
from os import path as op

import numpy as np
import mne

data_path = pathlib.Path(mne.datasets.sample.data_path())
subjects_dir = data_path / 'subjects'
subject = 'sample'
meg_path = data_path / 'MEG' / subject

data_path = mne.datasets.sample.data_path()
subjects_dir = op.join(data_path, 'subjects')
fname_ave = op.join(data_path, 'MEG', 'sample', 'sample_audvis-ave.fif')
fname_bem = op.join(subjects_dir, 'sample', 'bem',
                    'sample-5120-5120-5120-bem-sol.fif')
bem = mne.read_bem_solution(fname_bem, verbose=False)
fname_trans = op.join(data_path, 'MEG', 'sample',
                      'sample_audvis_raw-trans.fif')
info = mne.io.read_info(fname_ave, verbose=False)
info['bads'] = []
info['projs'] = []
picks = mne.pick_types(info, meg=True, eeg=True)
info = mne.pick_info(info, picks)

x_dip, y_dip, z_dip = 20, 20, 40
ori = np.array([[1, 0, 0]])
dip_pos = (x_dip, y_dip, z_dip)
pos = np.array([dip_pos]) / 1e3
dip = mne.Dipole(times=[0], pos=pos, amplitude=[10e-9], ori=ori, gof=[100])
fwd, _ = mne.make_forward_dipole(dip, fname_bem, info, fname_trans)
mne.write_forward_solution('tmp-fwd.fif', fwd, overwrite=True)
```

gives

```
---------------------------------------------------------------------------
KeyError                                  Traceback (most recent call last)
<ipython-input-4-789d589b9079> in <module>
     30 dip = mne.Dipole(times=[0], pos=pos, amplitude=[10e-9], ori=ori, gof=[100])
     31 fwd, _ = mne.make_forward_dipole(dip, fname_bem, info, fname_trans)
---> 32 mne.write_forward_solution('tmp-fwd.fif', fwd, overwrite=True)

<decorator-gen-258> in write_forward_solution(fname, fwd, overwrite, verbose)

~/work/src/mne-python/mne/forward/forward.py in write_forward_solution(fname, fwd, overwrite, verbose)
    777     # Write the source spaces (again)
    778     #
--> 779     _write_source_spaces_to_fid(fid, src)
    780     n_vert = sum([ss['nuse'] for ss in src])
    781     if fwd['_orig_source_ori'] == FIFF.FIFFV_MNE_FIXED_ORI:

<decorator-gen-87> in _write_source_spaces_to_fid(fid, src, verbose)

~/work/src/mne-python/mne/source_space.py in _write_source_spaces_to_fid(fid, src, verbose)
    984         logger.info('    Write a source space...')
    985         start_block(fid, FIFF.FIFFB_MNE_SOURCE_SPACE)
--> 986         _write_one_source_space(fid, s, verbose)
    987         end_block(fid, FIFF.FIFFB_MNE_SOURCE_SPACE)
    988         logger.info('    [done]')

~/work/src/mne-python/mne/source_space.py in _write_one_source_space(fid, this, verbose)
   1064     write_int(fid, FIFF.FIFF_MNE_SOURCE_SPACE_NUSE, this['nuse'])
   1065 
-> 1066     if this['ntri'] > 0:
   1067         write_int(fid, FIFF.FIFF_MNE_SOURCE_SPACE_NTRI, this['ntri'])
   1068         write_int_matrix(fid, FIFF.FIFF_MNE_SOURCE_SPACE_TRIANGLES,

KeyError: 'ntri'
```

cc @hoechenberger 


Discussion:
https://mne.tools/stable/generated/mne.make_forward_dipole.html

`make_forward_dipole` returns a tuple of `fwd`, `stc`
now the bug report is clear
Yes, now it's a bug and not a usage error :)

Resolving patch:
diff --git a/mne/forward/_make_forward.py b/mne/forward/_make_forward.py
--- a/mne/forward/_make_forward.py
+++ b/mne/forward/_make_forward.py
@@ -25,7 +25,7 @@
 from ..utils import logger, verbose, warn, _pl
 from ..parallel import check_n_jobs
 from ..source_space import (_ensure_src, _filter_source_spaces,
-                            _make_discrete_source_space, SourceSpaces)
+                            _make_discrete_source_space, _complete_vol_src)
 from ..source_estimate import VolSourceEstimate
 from ..surface import _normalize_vectors
 from ..bem import read_bem_solution, _bem_find_surface, ConductorModel
@@ -676,8 +676,8 @@ def make_forward_dipole(dipole, bem, info, trans=None, n_jobs=1, verbose=None):
     # NB information about dipole orientation enters here, then no more
     sources = dict(rr=pos, nn=ori)
     # Dipole objects must be in the head frame
-    sp = _make_discrete_source_space(sources, coord_frame='head')
-    src = SourceSpaces([sp])  # dict with working_dir, command_line not nec
+    src = _complete_vol_src(
+        [_make_discrete_source_space(sources, coord_frame='head')])
 
     # Forward operator created for channels in info (use pick_info to restrict)
     # Use defaults for most params, including min_dist
diff --git a/mne/source_space.py b/mne/source_space.py
--- a/mne/source_space.py
+++ b/mne/source_space.py
@@ -1744,6 +1744,11 @@ def setup_volume_source_space(subject=None, pos=5.0, mri=None,
             del s['vol_dims']
 
     # Save it
+    sp = _complete_vol_src(sp, subject)
+    return sp
+
+
+def _complete_vol_src(sp, subject=None):
     for s in sp:
         s.update(dict(nearest=None, dist=None, use_tris=None, patch_inds=None,
                       dist_limit=None, pinfo=None, ntri=0, nearest_dist=None,


Test patch:
diff --git a/mne/forward/tests/test_forward.py b/mne/forward/tests/test_forward.py
--- a/mne/forward/tests/test_forward.py
+++ b/mne/forward/tests/test_forward.py
@@ -30,21 +30,22 @@
                        'data', 'test-ave.fif')
 
 
-def compare_forwards(f1, f2):
+def assert_forward_allclose(f1, f2, rtol=1e-7):
     """Compare two potentially converted forward solutions."""
-    assert_allclose(f1['sol']['data'], f2['sol']['data'])
-    assert_equal(f1['sol']['ncol'], f2['sol']['ncol'])
-    assert_equal(f1['sol']['ncol'], f1['sol']['data'].shape[1])
-    assert_allclose(f1['source_nn'], f2['source_nn'])
+    assert_allclose(f1['sol']['data'], f2['sol']['data'], rtol=rtol)
+    assert f1['sol']['ncol'] == f2['sol']['ncol']
+    assert f1['sol']['ncol'] == f1['sol']['data'].shape[1]
+    assert_allclose(f1['source_nn'], f2['source_nn'], rtol=rtol)
     if f1['sol_grad'] is not None:
         assert (f2['sol_grad'] is not None)
         assert_allclose(f1['sol_grad']['data'], f2['sol_grad']['data'])
-        assert_equal(f1['sol_grad']['ncol'], f2['sol_grad']['ncol'])
-        assert_equal(f1['sol_grad']['ncol'], f1['sol_grad']['data'].shape[1])
+        assert f1['sol_grad']['ncol'] == f2['sol_grad']['ncol']
+        assert f1['sol_grad']['ncol'] == f1['sol_grad']['data'].shape[1]
     else:
         assert (f2['sol_grad'] is None)
-    assert_equal(f1['source_ori'], f2['source_ori'])
-    assert_equal(f1['surf_ori'], f2['surf_ori'])
+    assert f1['source_ori'] == f2['source_ori']
+    assert f1['surf_ori'] == f2['surf_ori']
+    assert f1['src'][0]['coord_frame'] == f1['src'][0]['coord_frame']
 
 
 @testing.requires_testing_data
@@ -62,7 +63,7 @@ def test_convert_forward():
     fwd_new = convert_forward_solution(fwd_surf, surf_ori=False)
     assert (repr(fwd_new))
     assert (isinstance(fwd_new, Forward))
-    compare_forwards(fwd, fwd_new)
+    assert_forward_allclose(fwd, fwd_new)
     del fwd_new
     gc.collect()
 
@@ -79,7 +80,7 @@ def test_convert_forward():
                                        force_fixed=False)
     assert (repr(fwd_new))
     assert (isinstance(fwd_new, Forward))
-    compare_forwards(fwd, fwd_new)
+    assert_forward_allclose(fwd, fwd_new)
     del fwd, fwd_new, fwd_fixed
     gc.collect()
 
@@ -124,7 +125,7 @@ def test_io_forward(tmpdir):
     assert (repr(fwd_read))
     assert (isinstance(fwd_read, Forward))
     assert (is_fixed_orient(fwd_read))
-    compare_forwards(fwd, fwd_read)
+    assert_forward_allclose(fwd, fwd_read)
 
     fwd = convert_forward_solution(fwd, surf_ori=True, force_fixed=True,
                                    use_cps=True)
@@ -143,7 +144,7 @@ def test_io_forward(tmpdir):
     assert (repr(fwd_read))
     assert (isinstance(fwd_read, Forward))
     assert (is_fixed_orient(fwd_read))
-    compare_forwards(fwd, fwd_read)
+    assert_forward_allclose(fwd, fwd_read)
 
     fwd = read_forward_solution(fname_meeg_grad)
     fwd = convert_forward_solution(fwd, surf_ori=True, force_fixed=True,
@@ -163,7 +164,7 @@ def test_io_forward(tmpdir):
     assert (repr(fwd_read))
     assert (isinstance(fwd_read, Forward))
     assert (is_fixed_orient(fwd_read))
-    compare_forwards(fwd, fwd_read)
+    assert_forward_allclose(fwd, fwd_read)
 
     # test warnings on bad filenames
     fwd = read_forward_solution(fname_meeg_grad)
@@ -176,7 +177,7 @@ def test_io_forward(tmpdir):
     fwd = read_forward_solution(fname_meeg)
     write_forward_solution(fname_temp, fwd, overwrite=True)
     fwd_read = read_forward_solution(fname_temp)
-    compare_forwards(fwd, fwd_read)
+    assert_forward_allclose(fwd, fwd_read)
 
 
 @testing.requires_testing_data
@@ -280,7 +281,7 @@ def test_restrict_forward_to_stc(tmpdir):
     fwd_out_read = read_forward_solution(fname_copy)
     fwd_out_read = convert_forward_solution(fwd_out_read, surf_ori=True,
                                             force_fixed=False)
-    compare_forwards(fwd_out, fwd_out_read)
+    assert_forward_allclose(fwd_out, fwd_out_read)
 
 
 @testing.requires_testing_data
@@ -346,7 +347,7 @@ def test_restrict_forward_to_label(tmpdir):
     fname_copy = tmpdir.join('copy-fwd.fif')
     write_forward_solution(fname_copy, fwd_out, overwrite=True)
     fwd_out_read = read_forward_solution(fname_copy)
-    compare_forwards(fwd_out, fwd_out_read)
+    assert_forward_allclose(fwd_out, fwd_out_read)
 
 
 @testing.requires_testing_data
@@ -389,7 +390,7 @@ def test_average_forward_solution(tmpdir):
     # with gradient
     fwd = read_forward_solution(fname_meeg_grad)
     fwd_ave = average_forward_solutions([fwd, fwd])
-    compare_forwards(fwd, fwd_ave)
+    assert_forward_allclose(fwd, fwd_ave)
 
 
 @testing.requires_testing_data
diff --git a/mne/forward/tests/test_make_forward.py b/mne/forward/tests/test_make_forward.py
--- a/mne/forward/tests/test_make_forward.py
+++ b/mne/forward/tests/test_make_forward.py
@@ -26,6 +26,8 @@
 from mne.source_space import (get_volume_labels_from_aseg, write_source_spaces,
                               _compare_source_spaces, setup_source_space)
 
+from mne.forward.tests.test_forward import assert_forward_allclose
+
 data_path = testing.data_path(download=False)
 fname_meeg = op.join(data_path, 'MEG', 'sample',
                      'sample_audvis_trunc-meg-eeg-oct-4-fwd.fif')
@@ -340,7 +342,7 @@ def test_forward_mixed_source_space(tmpdir):
 
 @pytest.mark.slowtest
 @testing.requires_testing_data
-def test_make_forward_dipole():
+def test_make_forward_dipole(tmpdir):
     """Test forward-projecting dipoles."""
     rng = np.random.RandomState(0)
 
@@ -436,6 +438,14 @@ def test_make_forward_dipole():
 
     dip_even_samp = Dipole(times, pos, amplitude, ori, gof)
 
+    # I/O round-trip
+    fname = str(tmpdir.join('test-fwd.fif'))
+    with pytest.warns(RuntimeWarning, match='free orientation'):
+        write_forward_solution(fname, fwd)
+    fwd_read = convert_forward_solution(
+        read_forward_solution(fname), force_fixed=True)
+    assert_forward_allclose(fwd, fwd_read, rtol=1e-6)
+
     fwd, stc = make_forward_dipole(dip_even_samp, sphere, info,
                                    trans=fname_trans)
     assert isinstance(stc, VolSourceEstimate)