Repository: mne-tools/mne-python

Issue:
read_custom_montage() silently ignores unit kwarg when loading Theta-phi montages (and possibly others)
#### Describe the bug
`read_custom_montage()` accepts a `unit` kwarg …
https://github.com/mne-tools/mne-python/blob/d2a5660db534c2d67af514b3d89b08abb67aa696/mne/channels/montage.py#L956-L957

… but doesn't pass the `unit` kwarg down to `_read_theta_phi_in_degrees()`:
https://github.com/mne-tools/mne-python/blob/d2a5660db534c2d67af514b3d89b08abb67aa696/mne/channels/montage.py#L1039-L1044

This is understandable, since `_read_theta_phi_in_degrees()` currently doesn't _accept_ a `unit` kwarg!
https://github.com/mne-tools/mne-python/blob/d2a5660db534c2d67af514b3d89b08abb67aa696/mne/channels/_standard_montage_utils.py#L238-L239

The result is a montage built on the assumption that head size was specified in meters, regardless of the `unit` kwarg the user passes.

#### Steps to reproduce
```python
import pathlib
import mne


base_dir = pathlib.Path('/Users/hoechenberger/Projects/TaDe')
montage_file = base_dir / 'actiCHamp_64Ch_Theta-phi_in_deg_including_fid.txt'

head_sizes = (0.085, 85)
units = ('m', 'mm')

for head_size, unit in zip(head_sizes, units):
    m = mne.channels.read_custom_montage(montage_file,
                                         head_size=head_size,
                                         unit=unit)
    print(f'Specfiied head size: {head_size} {unit}')
    print(f'Resulting LPA loc:\n{m.dig[0]}\n')
```
(I can upload the files if need be, but I think the example is pretty straightforward)

#### Expected results
Both function calls should yield identical channel locations.

#### Actual results
The channel locations differ, as the head size is always assumed to have been specified in meters:
```
Specfiied head size: 0.085 m
Resulting LPA loc:
<DigPoint |        LPA : (-78.8, -0.0, -31.8) mm   : unknown frame>

Specfiied head size: 85 mm
Resulting LPA loc:
<DigPoint |        LPA : (-78810.6, -0.0, -31841.6) mm : unknown frame>
```

#### Additional information
Since the `unit` kwarg isn't processed correctly, it's also not sanity-checked for invalid values. So one could pass anything, without creating an error message:

```python
mne.channels.read_custom_montage(montage_file, unit='hello world!')
```
Output:
```
<DigMontage | 0 extras (headshape), 0 HPIs, 3 fiducials, 65 channels>
```

A number of other montage-reading functions may be affected too, as they also seem to be assuming the head size is always specified in meters:

- `_read_elc`
- `_read_csd`
- `_read_sfp`
- `_read_elp_besa`



Discussion:
PR welcome ! thx for the bug report


Resolving patch:
diff --git a/mne/channels/_standard_montage_utils.py b/mne/channels/_standard_montage_utils.py
--- a/mne/channels/_standard_montage_utils.py
+++ b/mne/channels/_standard_montage_utils.py
@@ -294,14 +294,12 @@ def _read_elp_besa(fname, head_size):
     return make_dig_montage(ch_pos=ch_pos, nasion=nasion, lpa=lpa, rpa=rpa)
 
 
-def _read_brainvision(fname, head_size, unit):
+def _read_brainvision(fname, head_size):
     # 'BrainVision Electrodes File' format
     # Based on BrainVision Analyzer coordinate system: Defined between
     # standard electrode positions: X-axis from T7 to T8, Y-axis from Oz to
     # Fpz, Z-axis orthogonal from XY-plane through Cz, fit to a sphere if
     # idealized (when radius=1), specified in millimeters
-    if unit not in ['auto', 'mm']:
-        raise ValueError('`unit` must be "auto" or "mm" for .bvef files.')
     root = ElementTree.parse(fname).getroot()
     ch_names = [s.text for s in root.findall("./Electrode/Name")]
     theta = [float(s.text) for s in root.findall("./Electrode/Theta")]
diff --git a/mne/channels/montage.py b/mne/channels/montage.py
--- a/mne/channels/montage.py
+++ b/mne/channels/montage.py
@@ -943,7 +943,7 @@ def read_polhemus_fastscan(fname, unit='mm'):
     return points
 
 
-def _read_eeglab_locations(fname, unit):
+def _read_eeglab_locations(fname):
     ch_names = np.genfromtxt(fname, dtype=str, usecols=3).tolist()
     topo = np.loadtxt(fname, dtype=float, usecols=[1, 2])
     sph = _topo_to_sph(topo)
@@ -953,8 +953,7 @@ def _read_eeglab_locations(fname, unit):
     return ch_names, pos
 
 
-def read_custom_montage(fname, head_size=HEAD_SIZE_DEFAULT, unit='m',
-                        coord_frame=None):
+def read_custom_montage(fname, head_size=HEAD_SIZE_DEFAULT, coord_frame=None):
     """Read a montage from a file.
 
     Parameters
@@ -963,12 +962,11 @@ def read_custom_montage(fname, head_size=HEAD_SIZE_DEFAULT, unit='m',
         File extension is expected to be:
         '.loc' or '.locs' or '.eloc' (for EEGLAB files),
         '.sfp' (BESA/EGI files), '.csd',
-        ‘.elc’, ‘.txt’, ‘.csd’, ‘.elp’ (BESA spherical),
-        .bvef (BrainVision files).
-
+        '.elc', '.txt', '.csd', '.elp' (BESA spherical),
+        '.bvef' (BrainVision files).
     head_size : float | None
-        The size of the head in [m]. If none, returns the values read from the
-        file with no modification. Defaults to 95mm.
+        The size of the head in meters. If `None`, returns the values read from
+        the montage file with no modification. Defaults to 0.095m.
     coord_frame : str | None
         The coordinate frame of the points. Usually this is "unknown"
         for native digitizer space. Defaults to None, which is "unknown" for
@@ -1018,7 +1016,7 @@ def read_custom_montage(fname, head_size=HEAD_SIZE_DEFAULT, unit='m',
         if head_size is None:
             raise ValueError(
                 "``head_size`` cannot be None for '{}'".format(ext))
-        ch_names, pos = _read_eeglab_locations(fname, unit)
+        ch_names, pos = _read_eeglab_locations(fname)
         scale = head_size / np.median(np.linalg.norm(pos, axis=-1))
         pos *= scale
 
@@ -1047,7 +1045,7 @@ def read_custom_montage(fname, head_size=HEAD_SIZE_DEFAULT, unit='m',
         montage = _read_elp_besa(fname, head_size)
 
     elif ext in SUPPORTED_FILE_EXT['brainvision']:
-        montage = _read_brainvision(fname, head_size, unit)
+        montage = _read_brainvision(fname, head_size)
 
     if coord_frame is not None:
         coord_frame = _coord_frame_const(coord_frame)


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
@@ -146,7 +146,7 @@ def test_documented():
 
 @pytest.mark.parametrize('reader, file_content, expected_dig, ext', [
     pytest.param(
-        partial(read_custom_montage, head_size=None, unit='m'),
+        partial(read_custom_montage, head_size=None),
         ('FidNz 0       9.071585155     -2.359754454\n'
          'FidT9 -6.711765       0.040402876     -3.251600355\n'
          'very_very_very_long_name -5.831241498 -4.494821698  4.955347697\n'
@@ -163,7 +163,7 @@ def test_documented():
         'sfp', id='sfp'),
 
     pytest.param(
-        partial(read_custom_montage, head_size=1, unit='n/a'),
+        partial(read_custom_montage, head_size=1),
         ('1	       0	 0.50669	     FPz\n'
          '2	      23	 0.71	    	EOG1\n'
          '3	 -39.947	 0.34459	      F3\n'
@@ -180,8 +180,7 @@ def test_documented():
         'loc', id='EEGLAB'),
 
     pytest.param(
-        partial(read_custom_montage, head_size=None, unit='m',
-                coord_frame='mri'),
+        partial(read_custom_montage, head_size=None, coord_frame='mri'),
         ('// MatLab   Sphere coordinates [degrees]         Cartesian coordinates\n'  # noqa: E501
          '// Label       Theta       Phi    Radius         X         Y         Z       off sphere surface\n'  # noqa: E501
          'E1      37.700     -14.000       1.000    0.7677    0.5934   -0.2419  -0.00000000000000011\n'  # noqa: E501
@@ -200,7 +199,7 @@ def test_documented():
         'csd', id='matlab'),
 
     pytest.param(
-        partial(read_custom_montage, head_size=None, unit='not_used'),
+        partial(read_custom_montage, head_size=None),
         ('# ASA electrode file\nReferenceLabel  avg\nUnitPosition    mm\n'
          'NumberPositions=    68\n'
          'Positions\n'
@@ -220,7 +219,7 @@ def test_documented():
         'elc', id='ASA electrode'),
 
     pytest.param(
-        partial(read_custom_montage, head_size=1, unit='n/a'),
+        partial(read_custom_montage, head_size=1),
         ('Site  Theta  Phi\n'
          'Fp1  -92    -72\n'
          'Fp2   92     72\n'
@@ -238,7 +237,7 @@ def test_documented():
         'txt', id='generic theta-phi (txt)'),
 
     pytest.param(
-        partial(read_custom_montage, head_size=None, unit='n/a'),
+        partial(read_custom_montage, head_size=None),
         ('346\n'  # XXX: this should actually race an error 346 != 4
          'FID\t      LPA\t -120.03\t      0\t      85\n'
          'FID\t      RPA\t  120.03\t      0\t      85\n'
@@ -276,7 +275,7 @@ def test_documented():
         'hpts', id='legacy mne-c'),
 
     pytest.param(
-        partial(read_custom_montage, head_size=None, unit='mm'),
+        partial(read_custom_montage, head_size=None),
         ('<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n'
          '<!-- Generated by EasyCap Configurator 19.05.2014 -->\n'
          '<Electrodes defaults="false">\n'