Repository: mne-tools/mne-python

Issue:
Brainvision does not read meas_date properly
The `raw.info['meas_date']` is defined in the [docs](https://martinos.org/mne/dev/generated/mne.Info.html#mne.Info) to do this:

```
meas_date : list of int

    The first element of this list is a POSIX timestamp
 (milliseconds since 1970-01-01 00:00:00)
denoting the date and time at which the measurement was taken.
The second element is the number of microseconds.

```
First of all: Why would we need a list of
- POSIX miliseconds
- and then POSIX microseconds?

Second: When reading brainvision data, the following is executed concerning `raw.info['meas_date]`

https://github.com/mne-tools/mne-python/blob/241161d00d685c988a5937888f8c76959d0cd224/mne/io/brainvision/brainvision.py#L463

Was this perhaps intended as a temporary placeholder but it got forgotten? It is not a list and it does not contain the measurement date but the date at reading of the file.

The actual recording date can be obtained from the .vmrk file. See the documentation from Brain Products below:

![image](https://user-images.githubusercontent.com/9084751/43080197-6fd22da6-8e8f-11e8-8a73-459b98272491.png)

So it usually gets written by the Brain Products equipment at the event called "New Segment". I can make a PR if you agree that the current code is strange.


Discussion:
`time.time()` is wrong, yes please read from the file if you can
> Why would we need a list of
>
> POSIX miliseconds
> and then POSIX microseconds?

Probably due to file storage specifications of FIFF
(microseconds are *in addition to* the specified number of milliseconds)
indeed this needs improvement.

please open a PR.

thanks

> (microseconds are in addition to the specified number of milliseconds)

This still does not make sense to me. See this example:

```python
# Parse BrainVision date
date_str = '20131113161403794232'
meas_date = datetime.strptime(date_str, '%Y%m%d%H%M%S%f')

# Convert to format for FIF meas_date
# We need list of unix time ... and unix time in microseconds
meas_date_unix = time.mktime(meas_date.timetuple())
meas_date_unix_micro = meas_date_unix * 1000000.
raw.info['meas_date'] = [meas_date_unix, meas_date_unix_micro]
```

This would give us `[1384355643.0, 1384355643000000.0]`. How does that help? 



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
@@ -14,10 +14,12 @@
 import os
 import os.path as op
 import re
-import time
+from datetime import datetime
+from math import modf
 
 import numpy as np
 
+from ..write import DATE_NONE
 from ...utils import verbose, logger, warn
 from ..constants import FIFF
 from ..meas_info import _empty_info
@@ -457,10 +459,36 @@ def _get_vhdr_info(vhdr_fname, eog, misc, scale, montage):
         fmt = dict((key, cfg.get('ASCII Infos', key))
                    for key in cfg.options('ASCII Infos'))
 
-    # locate EEG and marker files
+    # locate EEG binary file and marker file for the stim channel
     path = op.dirname(vhdr_fname)
     data_filename = op.join(path, cfg.get('Common Infos', 'DataFile'))
-    info['meas_date'] = int(time.time())
+    mrk_fname = op.join(path, cfg.get('Common Infos', 'MarkerFile'))
+
+    # Try to get measurement date from marker file
+    # Usually saved with a marker "New Segment", see BrainVision documentation
+    regexp = r'^Mk\d+=New Segment,.*,\d+,\d+,\d+,(\d{20})$'
+    with open(mrk_fname, 'r') as tmp_mrk_f:
+        lines = tmp_mrk_f.readlines()
+
+    for line in lines:
+        match = re.findall(regexp, line.strip())
+
+        # Always take first measurement date we find
+        if match:
+            date_str = match[0]
+            meas_date = datetime.strptime(date_str, '%Y%m%d%H%M%S%f')
+
+            # We need list of unix time in milliseconds and as second entry
+            # the additional amount of microseconds
+            epoch = datetime.utcfromtimestamp(0)
+            unix_time = (meas_date - epoch).total_seconds()
+            unix_secs = int(modf(unix_time)[1])
+            microsecs = int(modf(unix_time)[0] * 1e6)
+            info['meas_date'] = [unix_secs, microsecs]
+            break
+
+    else:
+        info['meas_date'] = DATE_NONE
 
     # load channel labels
     nchan = cfg.getint('Common Infos', 'NumberOfChannels') + 1
@@ -747,8 +775,6 @@ def _get_vhdr_info(vhdr_fname, eog, misc, scale, montage):
             unit=unit, unit_mul=0.,  # always zero- mne manual pg. 273
             coord_frame=FIFF.FIFFV_COORD_HEAD))
 
-    # for stim channel
-    mrk_fname = op.join(path, cfg.get('Common Infos', 'MarkerFile'))
     info._update_redundant()
     info._check_consistency()
     return info, data_filename, fmt, order, mrk_fname, montage, n_samples
diff --git a/mne/io/meas_info.py b/mne/io/meas_info.py
--- a/mne/io/meas_info.py
+++ b/mne/io/meas_info.py
@@ -147,9 +147,9 @@ class Info(dict):
     lowpass : float | None
         Lowpass corner frequency in Hertz.
     meas_date : list of int
-        The first element of this list is a POSIX timestamp (milliseconds since
+        The first element of this list is a UNIX timestamp (seconds since
         1970-01-01 00:00:00) denoting the date and time at which the
-        measurement was taken. The second element is the number of
+        measurement was taken. The second element is the additional number of
         microseconds.
     meas_id : dict | None
         The ID assigned to this measurement by the acquisition system or
@@ -410,8 +410,12 @@ def __repr__(self):
                 if len(entr) >= 56:
                     entr = _summarize_str(entr)
             elif k == 'meas_date' and np.iterable(v):
-                # first entry in meas_date is meaningful
-                entr = _stamp_to_dt(v).strftime('%Y-%m-%d %H:%M:%S') + ' GMT'
+                if np.array_equal(v, DATE_NONE):
+                    entr = 'unspecified'
+                else:
+                    # first entry in meas_date is meaningful
+                    entr = (_stamp_to_dt(v).strftime('%Y-%m-%d %H:%M:%S') +
+                            ' GMT')
             elif k == 'kit_system_id' and v is not None:
                 from .kit.constants import KIT_SYSNAMES
                 entr = '%i (%s)' % (v, KIT_SYSNAMES.get(v, 'unknown'))


Test patch:
diff --git a/mne/io/brainvision/tests/data/test_old_layout_latin1_software_filter.vmrk b/mne/io/brainvision/tests/data/test_old_layout_latin1_software_filter.vmrk
--- a/mne/io/brainvision/tests/data/test_old_layout_latin1_software_filter.vmrk
+++ b/mne/io/brainvision/tests/data/test_old_layout_latin1_software_filter.vmrk
@@ -12,3 +12,4 @@ DataFile=test_old_layout_latin1_software_filter.eeg
 ; Fields are delimited by commas, some fields might be omitted (empty).
 ; Commas in type or description text are coded as "".
 Mk1=New Segment,,1,1,0,20070716122240937454
+Mk2=New Segment,,2,1,0,20070716122240937455
diff --git a/mne/io/brainvision/tests/data/testv2.vmrk b/mne/io/brainvision/tests/data/testv2.vmrk
--- a/mne/io/brainvision/tests/data/testv2.vmrk
+++ b/mne/io/brainvision/tests/data/testv2.vmrk
@@ -11,7 +11,7 @@ DataFile=test.eeg
 ; <Size in data points>, <Channel number (0 = marker is related to all channels)>
 ; Fields are delimited by commas, some fields might be omitted (empty).
 ; Commas in type or description text are coded as "\1".
-Mk1=New Segment,,1,1,0,20131113161403794232
+Mk1=New Segment,,1,1,0
 Mk2=Stimulus,S253,487,1,0
 Mk3=Stimulus,S255,497,1,0
 Mk4=Stimulus,S254,1770,1,0
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
@@ -2,6 +2,7 @@
 from __future__ import print_function
 
 # Author: Teon Brooks <teon.brooks@gmail.com>
+#         Stefan Appelhoff <stefan.appelhoff@mailbox.org>
 #
 # License: BSD (3-clause)
 
@@ -20,6 +21,7 @@
 from mne.io.constants import FIFF
 from mne.io import read_raw_fif, read_raw_brainvision
 from mne.io.tests.test_raw import _test_raw_reader
+from mne.io.write import DATE_NONE
 
 FILE = inspect.getfile(inspect.currentframe())
 data_dir = op.join(op.dirname(op.abspath(FILE)), 'data')
@@ -61,6 +63,24 @@
 warnings.simplefilter('always')
 
 
+def test_vmrk_meas_date():
+    """Test successful extraction of measurement date."""
+    # Test file that does have a specific date
+    raw = read_raw_brainvision(vhdr_path)
+    assert_allclose(raw.info['meas_date'], [1384359243, 794231])
+    assert '2013-11-13 16:14:03 GMT' in repr(raw.info)
+
+    # Test file with multiple dates ... we should only take the first
+    raw = read_raw_brainvision(vhdr_old_path)
+    assert_allclose(raw.info['meas_date'], [1184588560, 937453])
+    assert '2007-07-16 12:22:40 GMT' in repr(raw.info)
+
+    # Test files with no date, we should get DATE_NONE from mne.io.write
+    raw = read_raw_brainvision(vhdr_v2_path)
+    assert_allclose(raw.info['meas_date'], DATE_NONE)
+    assert 'unspecified' in repr(raw.info)
+
+
 def test_vhdr_codepage_ansi():
     """Test BV reading with ANSI codepage."""
     raw_init = read_raw_brainvision(vhdr_path, event_id=event_id)