Repository: mne-tools/mne-python

Issue:
BUG: fix elp montage rotation
With .elp channel locations, without this patch:

![unknown-75](https://cloud.githubusercontent.com/assets/4321826/20154431/89ee194a-a6c7-11e6-9446-207cc6dcef06.png)

with this patch:

![unknown-8](https://cloud.githubusercontent.com/assets/4321826/20154414/7cba3538-a6c7-11e6-906d-80f73086e8af.png)

Possibly related to #3613 and/or #3690 ?

@Eric89GXL can you check? I'm really not sure.


Discussion:
```
======================================================================
FAIL: Test topo to sphere conversion.
----------------------------------------------------------------------
Traceback (most recent call last):
  File "C:\conda\envs\test\lib\site-packages\nose\case.py", line 197, in runTest
    self.test(*self.arg)
  File "C:\projects\mne-python\mne\tests\test_transforms.py", line 222, in test_topo_to_sph
    assert_allclose(pos, new[ii], atol=1e-7)
  File "C:\conda\envs\test\lib\site-packages\numpy\testing\utils.py", line 1297, in assert_allclose
    verbose=verbose, header=header)
  File "C:\conda\envs\test\lib\site-packages\numpy\testing\utils.py", line 665, in assert_array_compare
    raise AssertionError(msg)
AssertionError: 
Not equal to tolerance rtol=1e-07, atol=1e-07
(mismatch 100.0%)
 x: array([ 0.353553,  0.612372,  0.707107])
 y: array([-0.612372, -0.353553,  0.707107])
>>  raise AssertionError('\nNot equal to tolerance rtol=1e-07, atol=1e-07\n\n(mismatch 100.0%)\n x: array([ 0.353553,  0.612372,  0.707107])\n y: array([-0.612372, -0.353553,  0.707107])')
```

Not sure what is going on.
@wmvanvliet maybe you have an idea?

I agree that the fix makes the plots look correct but the original equations seem correct. do you know what's going on here? is the `.elp` coordinate system like left-handed? 

@jona-sassenhagen can you check with `git bisect` where it went wrong? I'm guessing it was one of my PRs... it would be good to beef up the tests more to make sure things get translated properly. If you don't have time let me know and I can look into it since I probably broke it.

FYI I've seen this type of rotations using eeglab .set files

cc @jmontoyam 

yeah, the fix doesn't look correct to me. Where did you get this `.elp` file from? Possible to add a test?

I've seen this types of rotations when converting eeglab files to fieldtrip (`eeglab2fieltrip` IIRC, but that was some time ago - that might have been fixed there already. eeglab and fieldtrip  topoplots were giving different head rotations at that time [at least for EGI channel locs])

I bet it's a bug in the file conversion but not in MNE-python no?

The error occurs with multiple Brainvision files and .elp files. These specific files gave correct results with previous versions of MNE.

I will try to find what version is to blame.

I can't really pinpoint the error because of the error introduced in #3613 and fixed in #3690, because the error specifically affects .elp reading. It is possible I introduced this specific error in my fix in #3690.

The last commit before #3613 works (2fa35d453670d759df3ff296c1f5c012f3f8a785).
Then #3613 makes .elps unreadable.
From #3690 on, .elps can be read, but they are rotated (when I did #3690, I only checked central topos that didn't change much under rotation :().

This also indicates IMO that the test modified in #3690 may be insufficient ... as it relies too much on shapes rather than values of the returned arrays.

@Eric89GXL can you indeed take a look? #3613 is very long and I'm really bad with transforms ...


Resolving patch:
diff --git a/mne/channels/montage.py b/mne/channels/montage.py
--- a/mne/channels/montage.py
+++ b/mne/channels/montage.py
@@ -268,7 +268,7 @@ def read_montage(kind, ch_names=None, path=None, unit='m', transform=False):
         az = data['f2']
         horiz = data['f3']
         radius = np.abs(az / 180.)
-        az = np.deg2rad(np.array([90. - h if a >= 0. else -90. - h
+        az = np.deg2rad(np.array([h if a >= 0. else 180 + h
                                   for h, a in zip(horiz, az)]))
         pol = radius * np.pi
         pos = _sph_to_cart(np.array([np.ones(len(az)) * 85., az, pol]).T)


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
@@ -40,51 +40,73 @@
 def test_montage():
     """Test making montages."""
     tempdir = _TempDir()
-    # no pep8
-    input_str = [
-        'FidNz 0.00000 10.56381 -2.05108\nFidT9 -7.82694 0.45386 -3.76056\n'
-        'very_very_very_long_name 7.82694 0.45386 -3.76056\nDmy 7.0 0.0 1.0',
-        '// 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
-        'E2      44.600      -0.880       1.000    0.7119    0.7021   -0.0154   0.00000000000000000\n'  # noqa: E501
-        'E3      51.700      11.000       1.000    0.6084    0.7704    0.1908   0.00000000000000000\n'  # noqa: E501
-        'E4      52.700      12.000       1.000    0.7084    0.7504    0.1508   0.00000000000000000',  # noqa: E501; dummy line
-        '# ASA electrode file\nReferenceLabel  avg\nUnitPosition    mm\n'
-        'NumberPositions=    68\nPositions\n-86.0761 -19.9897 -47.9860\n'
-        '85.7939 -20.0093 -48.0310\n0.0083 86.8110 -39.9830\n'
-        '85 -20 -48\n'
-        'Labels\nLPA\nRPA\nNz\nDummy\n',
-        '# ASA electrode file\nReferenceLabel  avg\nUnitPosition    m\n'
-        'NumberPositions=    68\nPositions\n-.0860761 -.0199897 -.0479860\n'
-        '.0857939 -.0200093 -.0480310\n.0000083 .00868110 -.0399830\n'
-        '.08 -.02 -.04\n'
-        'Labels\nLPA\nRPA\nNz\nDummy\n',
-        'Site  Theta  Phi\nFp1  -92    -72\nFp2   92     72\n'
-        'very_very_very_long_name   -60    -51\n'
-        'dummy -60 -52\n',
-        '346\n'
-        'EEG\t      F3\t -62.027\t -50.053\t      85\n'
-        'EEG\t      Fz\t  45.608\t      90\t      85\n'
-        'EEG\t      F4\t   62.01\t  50.103\t      85\n'
-        'EEG\t      FCz\t   68.01\t  58.103\t      85\n',
-        'eeg Fp1 -95.0 -3. -3.\neeg AF7 -1 -1 -3\neeg A3 -2 -2 2\neeg A 0 0 0'
-    ]
-
-    kinds = ['test.sfp', 'test.csd', 'test_mm.elc', 'test_m.elc', 'test.txt',
-             'test.elp', 'test.hpts']
-    for kind, text in zip(kinds, input_str):
-        fname = op.join(tempdir, kind)
+    inputs = dict(
+        sfp='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'
+            'Cz 0       0       8.899186843',
+        csd='// 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
+            'E3      51.700      11.000       1.000    0.6084    0.7704    0.1908   0.00000000000000000\n'  # noqa: E501
+            'E31      90.000     -11.000       1.000    0.0000    0.9816   -0.1908   0.00000000000000000\n'  # noqa: E501
+            'E61     158.000     -17.200       1.000   -0.8857    0.3579   -0.2957  -0.00000000000000022',  # noqa: E501
+        mm_elc='# ASA electrode file\nReferenceLabel  avg\nUnitPosition    mm\n'  # noqa:E501
+               'NumberPositions=    68\n'
+               'Positions\n'
+               '-86.0761 -19.9897 -47.9860\n'
+               '85.7939 -20.0093 -48.0310\n'
+               '0.0083 86.8110 -39.9830\n'
+               '-86.0761 -24.9897 -67.9860\n'
+               'Labels\nLPA\nRPA\nNz\nDummy\n',
+        m_elc='# ASA electrode file\nReferenceLabel  avg\nUnitPosition    m\n'
+              'NumberPositions=    68\nPositions\n-.0860761 -.0199897 -.0479860\n'  # noqa:E501
+              '.0857939 -.0200093 -.0480310\n.0000083 .00868110 -.0399830\n'
+              '.08 -.02 -.04\n'
+              'Labels\nLPA\nRPA\nNz\nDummy\n',
+        txt='Site  Theta  Phi\n'
+            'Fp1  -92    -72\n'
+            'Fp2   92     72\n'
+            'very_very_very_long_name       -92     72\n'
+            'O2        92    -90\n',
+        elp='346\n'
+            'EEG\t      F3\t -62.027\t -50.053\t      85\n'
+            'EEG\t      Fz\t  45.608\t      90\t      85\n'
+            'EEG\t      F4\t   62.01\t  50.103\t      85\n'
+            'EEG\t      FCz\t   68.01\t  58.103\t      85\n',
+        hpts='eeg Fp1 -95.0 -3. -3.\n'
+             'eeg AF7 -1 -1 -3\n'
+             'eeg A3 -2 -2 2\n'
+             'eeg A 0 0 0',
+    )
+    # Get actual positions and save them for checking
+    # csd comes from the string above, all others come from commit 2fa35d4
+    poss = dict(
+        sfp=[[0.0, 9.07159, -2.35975], [-6.71176, 0.0404, -3.2516],
+             [-5.83124, -4.49482, 4.95535], [0.0, 0.0, 8.89919]],
+        mm_elc=[[-0.08608, -0.01999, -0.04799], [0.08579, -0.02001, -0.04803],
+                [1e-05, 0.08681, -0.03998], [-0.08608, -0.02499, -0.06799]],
+        m_elc=[[-0.08608, -0.01999, -0.04799], [0.08579, -0.02001, -0.04803],
+               [1e-05, 0.00868, -0.03998], [0.08, -0.02, -0.04]],
+        txt=[[-26.25044, 80.79056, -2.96646], [26.25044, 80.79056, -2.96646],
+             [-26.25044, -80.79056, -2.96646], [0.0, -84.94822, -2.96646]],
+        elp=[[-48.20043, 57.55106, 39.86971], [0.0, 60.73848, 59.4629],
+             [48.1426, 57.58403, 39.89198], [41.64599, 66.91489, 31.8278]],
+        hpts=[[-95, -3, -3], [-1, -1., -3.], [-2, -2, 2.], [0, 0, 0]],
+    )
+    for key, text in inputs.items():
+        kind = key.split('_')[-1]
+        fname = op.join(tempdir, 'test.' + kind)
         with open(fname, 'w') as fid:
             fid.write(text)
         montage = read_montage(fname)
-        if ".sfp" in kind or ".txt" in kind:
+        if kind in ('sfp', 'txt'):
             assert_true('very_very_very_long_name' in montage.ch_names)
         assert_equal(len(montage.ch_names), 4)
         assert_equal(len(montage.ch_names), len(montage.pos))
         assert_equal(montage.pos.shape, (4, 3))
-        assert_equal(montage.kind, op.splitext(kind)[0])
-        if kind.endswith('csd'):
+        assert_equal(montage.kind, 'test')
+        if kind == 'csd':
             dtype = [('label', 'S4'), ('theta', 'f8'), ('phi', 'f8'),
                      ('radius', 'f8'), ('x', 'f8'), ('y', 'f8'), ('z', 'f8'),
                      ('off_sph', 'f8')]
@@ -92,9 +114,8 @@ def test_montage():
                 table = np.loadtxt(fname, skip_header=2, dtype=dtype)
             except TypeError:
                 table = np.loadtxt(fname, skiprows=2, dtype=dtype)
-            pos2 = np.c_[table['x'], table['y'], table['z']]
-            assert_array_almost_equal(pos2[:-1, :], montage.pos[:-1, :], 4)
-        if kind.endswith('elc'):
+            poss['csd'] = np.c_[table['x'], table['y'], table['z']]
+        if kind == 'elc':
             # Make sure points are reasonable distance from geometric centroid
             centroid = np.sum(montage.pos, axis=0) / montage.pos.shape[0]
             distance_from_centroid = np.apply_along_axis(
@@ -102,6 +123,7 @@ def test_montage():
                 montage.pos - centroid)
             assert_array_less(distance_from_centroid, 0.2)
             assert_array_less(0.01, distance_from_centroid)
+        assert_array_almost_equal(poss[key], montage.pos, 4, err_msg=key)
 
     # Test reading in different letter case.
     ch_names = ["F3", "FZ", "F4", "FC3", "FCz", "FC4", "C3", "CZ", "C4", "CP3",
@@ -118,8 +140,7 @@ def test_montage():
     cardinal 1 0 -91 -42
     cardinal 3 0 91 -42
     """
-    kind = 'test_fid.hpts'
-    fname = op.join(tempdir, kind)
+    fname = op.join(tempdir, 'test_fid.hpts')
     with open(fname, 'w') as fid:
         fid.write(input_str)
     montage = read_montage(op.join(tempdir, 'test_fid.hpts'), transform=True)