Repository: mne-tools/mne-python

Issue:
Deviating values while reading data from EDF and BrainVision formats
Hi!

I am currently building a BIDS Docker App that takes three iEEG data formats: MEF3, EDF and BrainVision. In order to read the EDF and Brainvision formats I am using MNE-tools (mne-python, latest stable version 0.19.2) in Python 3.8.

To test the reading of data in Python I converted one dataset into all three formats. While being successful at loading each of the data formats into python, the raw data values coming from MNE seem to differ (see the attached image below):
- The raw values that MNE returns from the Brainvision format seem to be the correct values but scaled down
- The raw values that MNE returns from the EDF format are partial correct, but are wrong on values below 0.

The `mne.io.read_raw_edf` and `mne.io.read_raw_brainvision` functions were used to read the data and I retrieved the raw values using the methods described here:
https://mne.tools/dev/auto_tutorials/raw/plot_10_raw_overview.html#extracting-data-from-raw-objects

In order to see if the cause was not in the data files, I tried two other methods to read the data:
- I read the exact same data files in Matlab using the Fieldtrip package and there the BrainVision and EDF did show the correct values. 
- I used another Python EDF reader (PyEdfLib), which also gave the correct values.

These led me to believe it is something MNE reader, rather than something in the data.


I am not sure whether there is a conversion or data casting step in the reading of the raw data that I am missing? Or whether this could be a bug in the MNE reading function?
@jasmainak @dorahermes

Hope you can help. If more information is needed, then please let me know...

Thanks in advance,
Max

----
Attached, results using different readers:
![formats2](https://user-images.githubusercontent.com/43676624/77209386-241d6400-6acc-11ea-84b6-6597e6188d32.png)



Discussion:
@MaxvandenBoom can you share an EDF file privately so we can debug this. Could be a silly bug in Edf reader. 
@MaxvandenBoom to me it looks like the BrainVision format is OK, it's just off by a factor of 1e6. MNE-Python always converts EEG data into Volts when reading, even if the value in the data container was in μV. The EDF values does look like a bug though.
(for more info about unit conversion in MNE-Python, see here: https://mne.tools/dev/overview/implementation.html#internal-representation-units)
@MaxvandenBoom can you explain how you converted your data to the three file formats? What format was the data originally in?
Interesting, I just wanted to add that the EDF values differ from the MEF3 and BrainVision even with PyEDFlib and FieldTrip. It looks like something funky going on either with the data type and/or with the scaling.

Either way, as others have already noted, we need all files as well as details on how you created these files.
@agramfort Yes definitely, I will prepare a dataset
@drammock Thank you, that indeed explains the BrainVision values.
@jasmainak The EDF and BrainVision formats were derived from MEF3 format, conversion was done using Fieldtrip
@cbrnr Indeed, I have noticed that as well, but wanted to focus on the obvious incorrect values. It would be nice to find an explanation for those fluctuations as well :)

I will supply some datasets and the conversion scripts I used in one go
For BrainVision: Thank you guys for resolving the BrainVision difference, this is super helpful!

For EDF: I wrote these files to EDF and BrainVision using Fieldtrip, but there seems already a difference between the written .edf and the original data, even if loaded in Fieldtrip, so this is not due to MNE Python (these data might actually be >16 bit, I'll check on our end). 

General question: Does anyone know of an already existing standard dataset that exists in EDF and BrainVision that we could test for the Docker/BIDS App? It would be great to have an example testset for BIDS app development, we'll try to make one if not. 
we have some tiny date used for testing here:

https://github.com/mne-tools/mne-testing-data/tree/master/
<https://github.com/mne-tools/mne-testing-data/tree/master/EDF>

mne has tests for IO on all these files. What would the app do?

That's awesome, thank you! 

The app will do an automated detection of early onsets in cortico-cortical evoked potential (CCEP) data. @MaxvandenBoom is making sure first that all the BIDS data formats can be read correctly and the units make sense. 
great

can you share the repo of where you plan to put this code? you'll be using
brainlife.io ?

>

It's still under development, but we'll be putting the N1 detection on my lab's gitHub site:
https://github.com/MultimodalNeuroimagingLab



thanks for sharing. Let us know how it goes and if we can help in some way.

>

@agramfort @dorahermes

I tried the BrainVision and EDF dataset you suggested from https://github.com/mne-tools/mne-testing-data/tree/master/

- The BrainVision data was read correctly given drammock's explanation
- The EDF data has the same bug with negative values. I read the file `\EDF\test_edf_overlapping_annotations.edf` using MNE, pyEDFlib and Fieldtrip. The results are shown in the attached image below.

This is the code I used in Python:

```
from mne.io import read_raw_edf, read_raw_brainvision
from pyedflib import EdfReader

# read the edf data using MNE
mne_edf = read_raw_edf('D:\mne-testing-data-master\mne-testing-data-master\EDF\\test_edf_overlapping_annotations.edf', eog=None, misc=None, stim_channel=False, preload=True, verbose=None)
mne_vals = mne_edf[0, 0:20][0]

# read the edf data using pyedflib
lib_edf = EdfReader('D:\mne-testing-data-master\mne-testing-data-master\EDF\\test_edf_overlapping_annotations.edf')
lib_vals = lib_edf.readSignal(0)[0:20]
```

This is the code I used in Matlab:

```
edf_data = ft_read_data('D:\mne-testing-data-master\mne-testing-data-master\EDF\test_edf_overlapping_annotations.edf');
edf_vals = edf_data(1, 1:20)
```

So I think you were right and it might be some silly bug in the MNE edf reader... Also, should the EDF internal values be in Volts?

---

![formats3](https://user-images.githubusercontent.com/43676624/77373476-5e8a3980-6d36-11ea-81a4-9bc398ce5119.png)

This appears to be due to doing `stim_channel=False`, which gets interpreted as `stim_channel=0`, i.e., the first channel is treated as STIM and hence non-negative/unsigned.
... which is because of this:
```
 isinstance(False, int)
```
Note that this is not documented behavior (`bool` is not in the docstring of accepted values), but we can at least add some aliases to avoid this.

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
@@ -1032,9 +1032,12 @@ def _check_stim_channel(stim_channel, ch_names,
     """Check that the stimulus channel exists in the current datafile."""
     DEFAULT_STIM_CH_NAMES = ['status', 'trigger']
 
-    if stim_channel is None:
+    if stim_channel is None or stim_channel is False:
         return [], []
 
+    if stim_channel is True:  # convenient aliases
+        stim_channel = 'auto'
+
     elif isinstance(stim_channel, str):
         if stim_channel == 'auto':
             if 'auto' in ch_names:
diff --git a/mne/utils/check.py b/mne/utils/check.py
--- a/mne/utils/check.py
+++ b/mne/utils/check.py
@@ -21,6 +21,10 @@ def _ensure_int(x, name='unknown', must_be='an int'):
     # This is preferred over numbers.Integral, see:
     # https://github.com/scipy/scipy/pull/7351#issuecomment-299713159
     try:
+        # someone passing True/False is much more likely to be an error than
+        # intentional usage
+        if isinstance(x, bool):
+            raise TypeError()
         x = int(operator.index(x))
     except TypeError:
         raise TypeError('%s must be %s, got %s' % (name, must_be, type(x)))


Test patch:
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
@@ -106,9 +106,16 @@ def test_bdf_crop_save_stim_channel(tmpdir):
 
 
 @testing.requires_testing_data
-def test_edf_reduced():
-    """Test EDF with various sampling rates."""
-    _test_raw_reader(read_raw_edf, input_fname=edf_reduced, verbose='error')
+@pytest.mark.parametrize('fname', [
+    edf_reduced,
+    edf_overlap_annot_path,
+])
+@pytest.mark.parametrize('stim_channel', (None, False, 'auto'))
+def test_edf_others(fname, stim_channel):
+    """Test EDF with various sampling rates and overlapping annotations."""
+    _test_raw_reader(
+        read_raw_edf, input_fname=fname, stim_channel=stim_channel,
+        verbose='error')
 
 
 def test_edf_data():
diff --git a/mne/io/tests/test_raw.py b/mne/io/tests/test_raw.py
--- a/mne/io/tests/test_raw.py
+++ b/mne/io/tests/test_raw.py
@@ -100,6 +100,12 @@ def _test_raw_reader(reader, test_preloading=True, test_kwargs=True,
     assert raw.__class__.__name__ in repr(raw)  # to test repr
     assert raw.info.__class__.__name__ in repr(raw.info)
     assert isinstance(raw.info['dig'], (type(None), list))
+    data_max = full_data.max()
+    data_min = full_data.min()
+    # these limits could be relaxed if we actually find data with
+    # huge values (in SI units)
+    assert data_max < 1e5
+    assert data_min > -1e5
     if isinstance(raw.info['dig'], list):
         for di, d in enumerate(raw.info['dig']):
             assert isinstance(d, DigPoint), (di, d)
diff --git a/mne/utils/tests/test_check.py b/mne/utils/tests/test_check.py
--- a/mne/utils/tests/test_check.py
+++ b/mne/utils/tests/test_check.py
@@ -18,7 +18,7 @@
 from mne.utils import (check_random_state, _check_fname, check_fname,
                        _check_subject, requires_mayavi, traits_test,
                        _check_mayavi_version, _check_info_inv, _check_option,
-                       check_version, _check_path_like)
+                       check_version, _check_path_like, _validate_type)
 data_path = testing.data_path(download=False)
 base_dir = op.join(data_path, 'MEG', 'sample')
 fname_raw = op.join(data_path, 'MEG', 'sample', 'sample_audvis_trunc_raw.fif')
@@ -174,3 +174,10 @@ def test_check_path_like():
     assert _check_path_like(str_path) is True
     assert _check_path_like(pathlib_path) is True
     assert _check_path_like(no_path) is False
+
+
+def test_validate_type():
+    """Test _validate_type."""
+    _validate_type(1, 'int-like')
+    with pytest.raises(TypeError, match='int-like'):
+        _validate_type(False, 'int-like')