Repository: mne-tools/mne-bids

Issue:
Next release – Bump pybv requirement to 0.4
We've fixed some serious issues here, which demand a new release; and I think once pybv 0.4 has been released, we should bump our requirement to that version, because otherwise we might end up writing … problematic BV files. WDYT? 

cc @sappelhoff 


Discussion:
See https://github.com/bids-standard/pybv/issues/60
yes, definitely agreed :+1: 
Will 0.4 also fix the roundtrip issue we encountered recently?
yes, see https://github.com/sappelhoff/pybv/pull/1 which is based off https://github.com/bids-standard/pybv/pull/58

I'll open that PR properly once https://github.com/bids-standard/pybv/pull/58 is merged, to avoid conflicts
done: https://github.com/bids-standard/pybv/releases/tag/v0.4.0

Resolving patch:
diff --git a/mne_bids/write.py b/mne_bids/write.py
--- a/mne_bids/write.py
+++ b/mne_bids/write.py
@@ -681,8 +681,8 @@ def _write_raw_brainvision(raw, bids_fname, events):
         The events as MNE-Python format ndaray.
 
     """
-    if not check_version('pybv', '0.3'):
-        raise ImportError('pybv >=0.3 is required for converting '
+    if not check_version('pybv', '0.4'):  # pragma: no cover
+        raise ImportError('pybv >=0.4 is required for converting '
                           'file to Brainvision format')
     from pybv import write_brainvision
     # Subtract raw.first_samp because brainvision marks events starting from
@@ -709,17 +709,20 @@ def _write_raw_brainvision(raw, bids_fname, events):
                'mne-bids. Please contact the mne-bids team about this.')
         raise RuntimeError(msg)
 
-    # XXX Always write as float32 to avoid precision loss during I/O roundtrip.
-    # See https://github.com/bids-standard/pybv/issues/45
-    fmt = 'binary_float32'
+    # We enforce conversion to float32 format
+    # XXX: pybv can also write to int16, to do that, we need to get
+    # original units of data prior to conversion, and add an optimization
+    # function to pybv that maximizes the resolution parameter while
+    # ensuring that int16 can represent the data in original units.
     if raw.orig_format != 'single':
         warn(f'Encountered data in "{raw.orig_format}" format. '
              f'Converting to float32.', RuntimeWarning)
 
-    # writing with a resolution 1e-9 should be precise enough to avoid loss of
-    # resolution for any kind of data.
-    resolution = 1e-9
-    unit = 'µV'  # for compatibility with BrainVision software
+    # Writing to float32 µV with 0.1 resolution are the pybv defaults,
+    # which guarantees accurate roundtrip for values >= 1e-7 µV
+    fmt = 'binary_float32'
+    resolution = 1e-1
+    unit = 'µV'
     write_brainvision(data=raw.get_data(),
                       sfreq=raw.info['sfreq'],
                       ch_names=raw.ch_names,


Test patch:
diff --git a/mne_bids/tests/test_write.py b/mne_bids/tests/test_write.py
--- a/mne_bids/tests/test_write.py
+++ b/mne_bids/tests/test_write.py
@@ -271,7 +271,7 @@ def test_create_fif(_bids_validate):
     _bids_validate(bids_root)
 
 
-@requires_version('pybv', '0.3')
+@requires_version('pybv', '0.4')
 @pytest.mark.filterwarnings(warning_str['channel_unit_changed'])
 def test_fif(_bids_validate):
     """Test functionality of the write_raw_bids conversion for fif."""
@@ -866,7 +866,7 @@ def test_vhdr(_bids_validate):
     assert len([f for f in os.listdir(data_path) if op.isfile(f)]) == 0
 
     # test anonymize and convert
-    if check_version('pybv', '0.3'):
+    if check_version('pybv', '0.4'):
         raw = _read_raw_brainvision(raw_fname)
         output_path = _test_anonymize(raw, bids_path)
         _bids_validate(output_path)
@@ -1084,7 +1084,7 @@ def test_eegieeg(dir_name, fname, reader, _bids_validate):
     assert len(list(data.values())[0]) == 2
 
     # check that scans list is properly converted to brainvision
-    if check_version('pybv', '0.3'):
+    if check_version('pybv', '0.4'):
         daysback_min, daysback_max = _get_anonymization_daysback(raw)
         daysback = (daysback_min + daysback_max) // 2
 
@@ -1189,7 +1189,7 @@ def test_eegieeg(dir_name, fname, reader, _bids_validate):
     assert coordsystem_json['iEEGCoordinateSystem'] == 'Other'
 
     # test anonymize and convert
-    if check_version('pybv', '0.3'):
+    if check_version('pybv', '0.4'):
         raw = reader(raw_fname)
 
         # XXX: Need to remove "Status" channel until pybv supports
@@ -1268,7 +1268,7 @@ def test_bdf(_bids_validate):
         write_raw_bids(raw, bids_path)
 
     # test anonymize and convert
-    if check_version('pybv', '0.3'):
+    if check_version('pybv', '0.4'):
         raw = _read_raw_bdf(raw_fname)
         # XXX: Need to remove "Status" channel until pybv supports
         # channels that are non-Volt
@@ -1321,7 +1321,7 @@ def test_set(_bids_validate):
     _bids_validate(bids_root)
 
     # test anonymize and convert
-    if check_version('pybv', '0.3'):
+    if check_version('pybv', '0.4'):
         with pytest.warns(RuntimeWarning,
                           match='Encountered data in "double" format'):
             output_path = _test_anonymize(raw, bids_path)