Repository: mne-tools/mne-python

Issue:
Montage in MRI space converted to head when saving to epoch.fif
#### Describe the bug
Montage in MRI space gets transformed to head space when saving to an epochs .fif file. The issue seems to be due to the saving and not the loading, but not 100% clear. See [here](https://mne.discourse.group/t/save-epochs-to-fif-with-mri-space-montage/5305)

#### Steps to reproduce
```
import mne
import numpy as np
from numpy.random import normal
import tempfile
import os


# Create random montage:
montage = mne.channels.make_dig_montage(ch_pos={"ch_1": np.array([1, 2, 3]), "ch_2": np.array([4, 5, 6]),
                                                "ch_3": np.array([7, 8, 9])}, coord_frame="mri")

# Create random data:
info = mne.create_info(ch_names=['ch_1', 'ch_2', 'ch_3'],
                       ch_types=['ecog'] * 3,
                       sfreq=100)
data = np.array([[normal(loc=0, scale=2, size=100), normal(loc=0, scale=2, size=100),
                  normal(loc=0, scale=2, size=100)],
                 [normal(loc=0, scale=2, size=100), normal(loc=0, scale=2, size=100),
                  normal(loc=0, scale=2, size=100)]])
epochs = mne.EpochsArray(data, info, tmin=-0.2, verbose="ERROR")
epochs.set_montage(montage, verbose="ERROR")

# Printing the epochs montage info:
print("Coordinate frames before saving:")
print(epochs.get_montage().dig[0]["coord_frame"])

# Creating a temporary directory
temp_dir = tempfile.mkdtemp()
# Generate a file name
fname = temp_dir + os.sep + "-epo.fif"
epochs.save(fname)

loaded_epochs = mne.read_epochs(fname, verbose="ERROR")
print("Coordinate frames after saving:")
print(loaded_epochs.get_montage().dig[0]["coord_frame"])
```

#### Expected results

>Coordinate frames before saving:
5 (FIFFV_COORD_MRI)
5 (FIFFV_COORD_MRI)


#### Actual results

>Coordinate frames before saving:
5 (FIFFV_COORD_MRI)
4 (FIFFV_COORD_HEAD)


#### Additional information

> Platform:       Windows-10-10.0.19044-SP0
Python:         3.9.7 | packaged by conda-forge | (default, Sep 29 2021, 19:15:42) [MSC v.1916 64 bit (AMD64)]
Executable:     C:\Users\alexander.lepauvre\Anaconda3\envs\mne_24\python.exe
CPU:            Intel64 Family 6 Model 142 Stepping 10, GenuineIntel: 8 cores
Memory:         15.9 GB
mne:            0.24.0
numpy:          1.21.4 {blas=NO_ATLAS_INFO, lapack=lapack}
scipy:          1.7.2
matplotlib:     3.4.3 {backend=Qt5Agg}
sklearn:        1.0.1
numba:          0.53.1
nibabel:        3.2.1
nilearn:        0.8.1
dipy:           1.4.1
cupy:           Not found
pandas:         1.3.4
mayavi:         4.7.2
pyvista:        0.32.1 {OpenGL 4.5.0 - Build 26.20.100.6860 via Intel(R) UHD Graphics 620}
pyvistaqt:      0.5.0
ipyvtklink:     Not found
vtk:            9.0.3
PyQt5:          5.12.3
ipympl:         Not found
mne_qt_browser: Not found




Discussion:
ok I looked a bit into it. The problem happens on write (not on read). Basically when we write dig points on epochs.save we just ignore the coord frame. We call write_dig_points without a coord_frame parameter. So on read it defaults to head...

I think it's something we can tackle during our sprint late August unless @larsoner is eager to fix it before.
Thanks @agramfort. In the mean time, is there any way I change the coordinate frame of the epochs object manually to override this issue? I am thinking something like:
```
for ch in montage.dig:
   montage.dig[ch] = "5 (FIFFV_COORD_MRI)"
```

Kind regards,

Alex
you can hack it like this:

from mne.io.constants import FIFF
for d in loaded_epochs.info["dig"]:
d['coord_frame'] = FIFF.FIFFV_COORD_MRI

> Message ID: ***@***.***>
>

Converting to head space is actually intended behavior:

MNE-Python prefers to have EEG channel locations in head space, always. So `.apply_montage` converts the montage to head space (if you get rid of the ` verbose='ERROR'` you can see the warning message saying there are no fiducials so an identity transform is assumed). We probably want to keep this. If we allow electrodes to be in all kinds of coordinate spaces, we have to verify the coordinate space every time we use channel locations.

What is the reason for wanting to store digitized channel locations in MRI space? It may be simpler not to try and fight MNE-Python on this.

Resolving patch:
diff --git a/mne/channels/montage.py b/mne/channels/montage.py
--- a/mne/channels/montage.py
+++ b/mne/channels/montage.py
@@ -668,11 +668,19 @@ def transform_to_head(montage):
     # Get fiducial points and their coord_frame
     native_head_t = compute_native_head_t(montage)
     montage = montage.copy()  # to avoid inplace modification
+
     if native_head_t['from'] != FIFF.FIFFV_COORD_HEAD:
         for d in montage.dig:
             if d['coord_frame'] == native_head_t['from']:
                 d['r'] = apply_trans(native_head_t, d['r'])
                 d['coord_frame'] = FIFF.FIFFV_COORD_HEAD
+            elif d['kind'] == FIFF.FIFFV_POINT_EEG:
+                raise RuntimeError(
+                    f'Could not transform EEG channel {d["ident"]} position '
+                    f'from {_verbose_frames[d["coord_frame"]]} to head '
+                    'coordinates. Fiducial points are either missing or '
+                    'specified in a different coordinate frame than the EEG '
+                    'channel locations.')
     return montage
 
 
diff --git a/mne/io/_digitization.py b/mne/io/_digitization.py
--- a/mne/io/_digitization.py
+++ b/mne/io/_digitization.py
@@ -300,7 +300,7 @@ def _get_fid_coords(dig, raise_error=True):
     if len(fid_coord_frames) > 0 and raise_error:
         if set(fid_coord_frames.keys()) != set(['nasion', 'lpa', 'rpa']):
             raise ValueError("Some fiducial points are missing (got %s)." %
-                             fid_coords.keys())
+                             fid_coord_frames.keys())
 
         if len(set(fid_coord_frames.values())) > 1:
             raise ValueError(


Test patch:
diff --git a/mne/channels/tests/test_montage.py b/mne/channels/tests/test_montage.py
--- a/mne/channels/tests/test_montage.py
+++ b/mne/channels/tests/test_montage.py
@@ -1210,6 +1210,17 @@ def test_transform_to_head_and_compute_dev_head_t():
             montage_polhemus
         ))
 
+    # Test errors when transforming without fiducials explicitly where points
+    # are tagged to be not in head or unknown coord space.
+    montage_without_fids = make_dig_montage(
+        ch_pos={"ch_1": np.array([1, 2, 3]),
+                "ch_2": np.array([4, 5, 6]),
+                "ch_3": np.array([7, 8, 9])},
+        coord_frame="mri")  # MRI coordinate space
+    with pytest.raises(RuntimeError, match='Could not transform EEG channel'):
+        with pytest.warns(RuntimeWarning, match='Fiducial point .* not found'):
+            transform_to_head(montage_without_fids)
+
 
 def test_set_montage_with_mismatching_ch_names():
     """Test setting a DigMontage with mismatching ch_names."""