Repository: mne-tools/mne-python

Issue:
Bug in mne.io.read_raw_gdf()
#### Describe the bug
Lowpass and highpass cut off frequencies are swapped when using `mne.io.read_raw_gdf()` to load a gdf file from the BCI IV 2b dataset.


#### Steps to reproduce

1. Download BCI IV 2b dataset from [here](http://www.bbci.de/competition/iv/) (it requires a name and an email)
2. Load one of the gdf files using `mne.io.read_raw_gdf()`

#### Expected results
`raw_gdf.info` should return:
```
<Info | 7 non-empty values
 bads: []
 ch_names: EEG:C3, EEG:Cz, EEG:C4, EOG:ch01, EOG:ch02, EOG:ch03
 chs: 6 EEG
 custom_ref_applied: False
 highpass: 0.5 Hz
 lowpass: 100 Hz
 meas_date: 2006-03-07 09:23:36 UTC
 nchan: 6
 projs: []
 sfreq: 250.0 Hz
>
```

Also `raw_gdf.plot()` should look like the figure below
![figure1](https://user-images.githubusercontent.com/9465194/100607553-c6584180-3313-11eb-9b65-07dd44971779.png)


#### Actual results
> `raw_gdf.info`
```
<Info | 7 non-empty values
 bads: []
 ch_names: EEG:C3, EEG:Cz, EEG:C4, EOG:ch01, EOG:ch02, EOG:ch03
 chs: 6 EEG
 custom_ref_applied: False
 highpass: 100.0 Hz
 lowpass: 0.5 Hz
 meas_date: 2006-03-07 09:23:36 UTC
 nchan: 6
 projs: []
 sfreq: 250.0 Hz
>
```
> `raw_gdf.plot()`
![figure2](https://user-images.githubusercontent.com/9465194/100607692-facbfd80-3313-11eb-8bb0-b74151c9628b.png)

#### Additional information
I'm using the 0.22.dev0 version of mne on Ubuntu 18.04




Discussion:
I can confirm the issue (my response to the mailing list assumed that you were referring to data set 2A, which is also available as GDF and does work fine).
so it works fine with on gdf file and not with another one?

is this a problem in the files or in the reader?

>

It's strange that only these files do not work whereas others work just fine. Our code seems to read the header fields correctly, according to the [GDF specifications (page 13)](https://arxiv.org/pdf/cs/0608052.pdf) the fields are read in correct order (lowpass, highpass, notch).

I did notice that filter information in the fixed header does contain the values `LP:     1 Hz; HP: 100.00 Hz; Notch: NaN` in the GDF files of data set 2B (but not 2A, where these values are correct in the file and therefore also correctly read by MNE).

Now GDF says that we shouldn't read filter settings from the fixed header, because these settings are stored in the variable header - which is exactly what we do. It seems that these (wrong) values are actually stored in the variable header as well, because we read 1 Hz for the lowpass settings and 100 Hz for the highpass settings. Therefore, I'm inclined to think that these files contain invalid values, but we read them correctly.

Maybe we should sanity-check these settings and ignore them if e.g. LP < HP? @schloegl could you please chime in and let me know what's going on here?
I can confirm, that the information in these GDF files is incorrect, 
Lowpass and HighPass information should be switched in data set 2B.

Of course, it would be possible to re-encode the data in the correct 
way, but after 10+ years there are too many copies around. Therefore, 
I'd recommend to document this flawed "property" of that data set.


Kind regards,
    Alois
Thank you Alois! So I guess we can close this issue. @apospraf you already figured out a workaround how to work with the data. Let me know if you have any further questions.

Resolving patch:
diff --git a/mne/io/edf/edf.py b/mne/io/edf/edf.py
--- a/mne/io/edf/edf.py
+++ b/mne/io/edf/edf.py
@@ -343,11 +343,11 @@ def _read_header(fname, exclude):
     logger.info('%s file detected' % ext.upper())
     if ext in ('bdf', 'edf'):
         return _read_edf_header(fname, exclude)
-    elif ext in ('gdf'):
+    elif ext == 'gdf':
         return _read_gdf_header(fname, exclude), None
     else:
         raise NotImplementedError(
-            'Only GDF, EDF, and BDF files are supported, got %s.' % ext)
+            f'Only GDF, EDF, and BDF files are supported, got {ext}.')
 
 
 def _get_info(fname, stim_channel, eog, misc, exclude, preload):
@@ -473,6 +473,13 @@ def _get_info(fname, stim_channel, eog, misc, exclude, preload):
     if np.isnan(info['lowpass']):
         info['lowpass'] = info['sfreq'] / 2.
 
+    if info['highpass'] > info['lowpass']:
+        warn(f'Highpass cutoff frequency {info["highpass"]} is greater than '
+             f'lowpass cutoff frequency {info["lowpass"]}, '
+             'setting values to 0 and Nyquist.')
+        info['highpass'] = 0.
+        info['lowpass'] = info['sfreq'] / 2.
+
     # Some keys to be consistent with FIF measurement info
     info['description'] = None
     edf_info['nsamples'] = int(edf_info['n_records'] * max_samp)


Test patch:
diff --git a/mne/datasets/testing/__init__.py b/mne/datasets/testing/__init__.py
--- a/mne/datasets/testing/__init__.py
+++ b/mne/datasets/testing/__init__.py
@@ -1,4 +1,4 @@
 """MNE testing dataset."""
 
 from ._testing import (data_path, requires_testing_data, get_version,
-                       _pytest_param)
+                       _pytest_param, _pytest_mark)
diff --git a/mne/datasets/testing/_testing.py b/mne/datasets/testing/_testing.py
--- a/mne/datasets/testing/_testing.py
+++ b/mne/datasets/testing/_testing.py
@@ -47,9 +47,7 @@ def _skip_testing_data():
 
 def requires_testing_data(func):
     """Skip testing data test."""
-    import pytest
-    return pytest.mark.skipif(_skip_testing_data(),
-                              reason='Requires testing dataset')(func)
+    return _pytest_mark()(func)
 
 
 def _pytest_param(*args, **kwargs):
@@ -59,5 +57,10 @@ def _pytest_param(*args, **kwargs):
     # turn anything that uses testing data into an auto-skipper by
     # setting params=[testing._pytest_param()], or by parametrizing functions
     # with testing._pytest_param(whatever)
-    return pytest.param(*args, **kwargs, marks=pytest.mark.skipif(
-        _skip_testing_data(), reason='Requires testing dataset'))
+    return pytest.param(*args, **kwargs, marks=_pytest_mark())
+
+
+def _pytest_mark():
+    import pytest
+    return pytest.mark.skipif(
+        _skip_testing_data(), reason='Requires testing dataset')
diff --git a/mne/io/edf/tests/test_edf.py b/mne/io/edf/tests/test_edf.py
--- a/mne/io/edf/tests/test_edf.py
+++ b/mne/io/edf/tests/test_edf.py
@@ -8,6 +8,7 @@
 #
 # License: BSD (3-clause)
 
+from functools import partial
 import os.path as op
 import inspect
 
@@ -20,14 +21,17 @@
 
 from mne import pick_types, Annotations
 from mne.datasets import testing
+from mne.fixes import nullcontext
 from mne.utils import requires_pandas
-from mne.io import read_raw_edf, read_raw_bdf, read_raw_fif
+from mne.io import read_raw_edf, read_raw_bdf, read_raw_fif, edf, read_raw_gdf
 from mne.io.tests.test_raw import _test_raw_reader
 from mne.io.edf.edf import (_get_edf_default_event_id, _read_annotations_edf,
-                            _read_ch, _parse_prefilter_string, _edf_str_int)
+                            _read_ch, _parse_prefilter_string, _edf_str_int,
+                            _read_edf_header, _read_header)
 from mne.io.pick import channel_indices_by_type, get_channel_type_constants
 from mne.annotations import events_from_annotations, read_annotations
 
+td_mark = testing._pytest_mark()
 
 FILE = inspect.getfile(inspect.currentframe())
 data_dir = op.join(op.dirname(op.abspath(FILE)), 'data')
@@ -457,3 +461,50 @@ def test_invalid_date(tmpdir):
 def test_empty_chars():
     """Test blank char support."""
     assert _edf_str_int(b'1819\x00 ') == 1819
+
+
+def _hp_lp_rev(*args, **kwargs):
+    out, orig_units = _read_edf_header(*args, **kwargs)
+    out['lowpass'], out['highpass'] = out['highpass'], out['lowpass']
+    # this will happen for test_edf_stim_resamp.edf
+    if len(out['lowpass']) and out['lowpass'][0] == '0.000' and \
+            len(out['highpass']) and out['highpass'][0] == '0.0':
+        out['highpass'][0] = '10.0'
+    return out, orig_units
+
+
+@pytest.mark.filterwarnings('ignore:.*too long.*:RuntimeWarning')
+@pytest.mark.parametrize('fname, lo, hi, warns', [
+    (edf_path, 256, 0, False),
+    (edf_uneven_path, 50, 0, False),
+    (edf_stim_channel_path, 64, 0, False),
+    pytest.param(edf_overlap_annot_path, 64, 0, False, marks=td_mark),
+    pytest.param(edf_reduced, 256, 0, False, marks=td_mark),
+    pytest.param(test_generator_edf, 100, 0, False, marks=td_mark),
+    pytest.param(edf_stim_resamp_path, 256, 0, True, marks=td_mark),
+])
+def test_hp_lp_reversed(fname, lo, hi, warns, monkeypatch):
+    """Test HP/LP reversed (gh-8584)."""
+    fname = str(fname)
+    raw = read_raw_edf(fname)
+    assert raw.info['lowpass'] == lo
+    assert raw.info['highpass'] == hi
+    monkeypatch.setattr(edf.edf, '_read_edf_header', _hp_lp_rev)
+    if warns:
+        ctx = pytest.warns(RuntimeWarning, match='greater than lowpass')
+        new_lo, new_hi = raw.info['sfreq'] / 2., 0.
+    else:
+        ctx = nullcontext()
+        new_lo, new_hi = lo, hi
+    with ctx:
+        raw = read_raw_edf(fname)
+    assert raw.info['lowpass'] == new_lo
+    assert raw.info['highpass'] == new_hi
+
+
+def test_degenerate():
+    """Test checking of some bad inputs."""
+    for func in (read_raw_edf, read_raw_bdf, read_raw_gdf,
+                 partial(_read_header, exclude=())):
+        with pytest.raises(NotImplementedError, match='Only.*txt.*'):
+            func(edf_txt_stim_channel_path)