Repository: mne-tools/mne-python

Issue:
BrainVision reader treatment of info['lowpass']
Currently the BV reader sets info['lowpass'] to the lowest filter that was applied to _any_ channel. I think this should be changed due to:
1. Inconsistent with how we treat this field in `raw.filter(..., picks=picks)`, which only sets it if all data channels are filtered.
2. Dangers during decimation. Users can easily assume data are sufficiently low-passed when they are not, and get aliasing during epoching.
3. Dangers during processing. Users can have artifacts they think they have suppressed (e.g., line noise) when they have not.

Advantages of the existing approach (supported by @jona-sassenhagen and @palday):
1. It is potentially more conservative from a filtering-induced artifact approach, at least for `info['highpass']`.
2. It is dangerous to say e.g. `info['lowpass'] = sfreq/2.` when some of the channels have actually been filtered, as users may think they can analyze data in regions that have been suppressed.

@agramfort @dengemann thoughts on this one?



Discussion:
+1

I can implement this (it is a trivial change from a code standpoint), but a few questions for further thought:
1. Should we implement some method / have an optional return value from the BV reader with a list of channels and filter settings so that the user can see what the original values were?
2. Does it make sense to have an API change to support the old behavior? Something like  `heterogeneous_filter='strongest'|'weakest'` with the default in 0.13 being `strongest` and changing to `weakest` in 0.14 (much like the recent changes to the FIR filter settings)? 

to me it's a bug and I would not change API for this.

it's a bug fix.

can this be closed?


Resolving patch:
diff --git a/mne/io/brainvision/brainvision.py b/mne/io/brainvision/brainvision.py
--- a/mne/io/brainvision/brainvision.py
+++ b/mne/io/brainvision/brainvision.py
@@ -502,29 +502,30 @@ def _get_vhdr_info(vhdr_fname, eog, misc, scale, montage):
                 # highpass relaxed / no filters
                 highpass = [float(filt) if filt not in ('NaN', 'Off', 'DC')
                             else np.Inf for filt in highpass]
-                info['highpass'] = np.min(np.array(highpass, dtype=np.float))
+                info['highpass'] = np.max(np.array(highpass, dtype=np.float))
                 # Coveniently enough 1 / np.Inf = 0.0, so this works for
                 # DC / no highpass filter
                 info['highpass'] = 1. / info['highpass']
 
                 # not exactly the cleanest use of FP, but this makes us
                 # more conservative in *not* warning.
-                if info['highpass'] == 0.0:
+                if info['highpass'] == 0.0 and len(set(highpass)) == 1:
                     # not actually heterogeneous in effect
                     # ... just heterogeneously disabled
                     heterogeneous_hp_filter = False
             else:
                 highpass = [float(filt) if filt not in ('NaN', 'Off', 'DC')
                             else 0.0 for filt in highpass]
-                info['highpass'] = np.max(np.array(highpass, dtype=np.float))
-                if info['highpass'] == 0.0:
+                info['highpass'] = np.min(np.array(highpass, dtype=np.float))
+                if info['highpass'] == 0.0 and len(set(highpass)) == 1:
                     # not actually heterogeneous in effect
                     # ... just heterogeneously disabled
                     heterogeneous_hp_filter = False
+
             if heterogeneous_hp_filter:
                 warn('Channels contain different highpass filters. '
-                     'Highest filter setting (%0.2f Hz) will be stored.'
-                     % info['highpass'])
+                     'Lowest (weakest) filter setting (%0.2f Hz) '
+                     'will be stored.' % info['highpass'])
 
         if len(lowpass) == 0:
             pass
@@ -542,35 +543,49 @@ def _get_vhdr_info(vhdr_fname, eog, misc, scale, montage):
                 # infinitely relaxed / no filters
                 lowpass = [float(filt) if filt not in ('NaN', 'Off')
                            else 0.0 for filt in lowpass]
-                info['lowpass'] = np.max(np.array(lowpass, dtype=np.float))
+                info['lowpass'] = np.min(np.array(lowpass, dtype=np.float))
                 try:
                     info['lowpass'] = 1. / info['lowpass']
                 except ZeroDivisionError:
-                    # No lowpass actually set (always Off or equivalent)
-                    # so we let lowpass be set in _empty_info
-                    del info['lowpass']
-                    # not actually heterogeneous in effect
-                    # ... just heterogeneously disabled
-                    heterogeneous_lp_filter = False
+                    if len(set(lowpass)) == 1:
+                        # No lowpass actually set for the weakest setting
+                        # so we set lowpass to the Nyquist frequency
+                        info['lowpass'] = info['sfreq'] / 2.
+                        # not actually heterogeneous in effect
+                        # ... just heterogeneously disabled
+                        heterogeneous_lp_filter = False
+                    else:
+                        # no lowpass filter is the weakest filter,
+                        # but it wasn't the only filter
+                        pass
             else:
                 # We convert channels with disabled filters to having
                 # infinitely relaxed / no filters
                 lowpass = [float(filt) if filt not in ('NaN', 'Off')
                            else np.Inf for filt in lowpass]
-                info['lowpass'] = np.min(np.array(lowpass, dtype=np.float))
+                info['lowpass'] = np.max(np.array(lowpass, dtype=np.float))
 
                 if np.isinf(info['lowpass']):
-                    # No lowpass actually set (always Off or equivalent)
-                    # so we let lowpass be set in _empty_info
-                    del info['lowpass']
-                    # not actually heterogeneous in effect
-                    # ... just heterogeneously disabled
-                    heterogeneous_lp_filter = False
+                    # No lowpass actually set for the weakest setting
+                    # so we set lowpass to the Nyquist frequency
+                    info['lowpass'] = info['sfreq'] / 2.
+                    if len(set(lowpass)) == 1:
+                        # not actually heterogeneous in effect
+                        # ... just heterogeneously disabled
+                        heterogeneous_lp_filter = False
 
             if heterogeneous_lp_filter:
+                # this isn't clean FP, but then again, we only want to provide
+                # the Nyquist hint when the lowpass filter was actually
+                # calculated from dividing the sampling frequency by 2, so the
+                # exact/direct comparison (instead of tolerance) makes sense
+                if info['lowpass'] == info['sfreq'] / 2.0:
+                    nyquist = ', Nyquist limit'
+                else:
+                    nyquist = ""
                 warn('Channels contain different lowpass filters. '
-                     'Lowest filter setting (%0.2f Hz) will be stored.'
-                     % info['lowpass'])
+                     'Highest (weakest) filter setting (%0.2f Hz%s) '
+                     'will be stored.' % (info['lowpass'], nyquist))
 
     # locate EEG and marker files
     path = os.path.dirname(vhdr_fname)


Test patch:
diff --git a/mne/io/brainvision/tests/test_brainvision.py b/mne/io/brainvision/tests/test_brainvision.py
--- a/mne/io/brainvision/tests/test_brainvision.py
+++ b/mne/io/brainvision/tests/test_brainvision.py
@@ -84,7 +84,7 @@ def test_brainvision_data_highpass_filters():
 
     assert_true(all(any([trg, lp, hp]) for trg, lp, hp in expected_warnings))
 
-    assert_equal(raw.info['highpass'], 0.2)
+    assert_equal(raw.info['highpass'], 0.1)
     assert_equal(raw.info['lowpass'], 250.)
 
     # Homogeneous highpass in Hertz
@@ -114,7 +114,7 @@ def test_brainvision_data_highpass_filters():
 
     assert_true(all(any([trg, lp, hp]) for trg, lp, hp in expected_warnings))
 
-    assert_equal(raw.info['highpass'], 10.)
+    assert_equal(raw.info['highpass'], 5.)
     assert_equal(raw.info['lowpass'], 250.)
 
 
@@ -149,7 +149,7 @@ def test_brainvision_data_lowpass_filters():
     assert_true(all(any([trg, lp, hp]) for trg, lp, hp in expected_warnings))
 
     assert_equal(raw.info['highpass'], 0.1)
-    assert_equal(raw.info['lowpass'], 125.)
+    assert_equal(raw.info['lowpass'], 250.)
 
     # Homogeneous lowpass in seconds
     with warnings.catch_warnings(record=True) as w:  # event parsing
@@ -179,7 +179,7 @@ def test_brainvision_data_lowpass_filters():
     assert_true(all(any([trg, lp, hp]) for trg, lp, hp in expected_warnings))
 
     assert_equal(raw.info['highpass'], 0.1)
-    assert_equal(raw.info['lowpass'], 125.)
+    assert_equal(raw.info['lowpass'], 250.)
 
 
 def test_brainvision_data_partially_disabled_hw_filters():
@@ -203,8 +203,8 @@ def test_brainvision_data_partially_disabled_hw_filters():
 
     assert_true(all(any([trg, lp, hp]) for trg, lp, hp in expected_warnings))
 
-    assert_equal(raw.info['highpass'], 0.1)
-    assert_equal(raw.info['lowpass'], 250.)
+    assert_equal(raw.info['highpass'], 0.)
+    assert_equal(raw.info['lowpass'], 500.)
 
 
 def test_brainvision_data_software_filters_latin1_global_units():