diff --git a/mne/io/nirx/nirx.py b/mne/io/nirx/nirx.py index fdd78ac236b..c569ea95d00 100644 --- a/mne/io/nirx/nirx.py +++ b/mne/io/nirx/nirx.py @@ -6,6 +6,7 @@ import glob as glob import re as re import os.path as op +import datetime as dt import numpy as np @@ -15,8 +16,7 @@ from ..meas_info import create_info, _format_dig_points from ...annotations import Annotations from ...transforms import apply_trans, _get_trans -from ...utils import logger, verbose, fill_doc -from ...utils import warn +from ...utils import logger, verbose, fill_doc, warn @fill_doc @@ -118,22 +118,47 @@ def __init__(self, fname, preload=False, verbose=None): # Parse required header fields + # Extract measurement date and time + datetime_str = hdr['GeneralInfo']['Date'] + hdr['GeneralInfo']['Time'] + meas_date = None + # Several formats have been observed so we try each in turn + for dt_code in ['"%a, %b %d, %Y""%H:%M:%S.%f"', + '"%a, %d %b %Y""%H:%M:%S.%f"']: + try: + meas_date = dt.datetime.strptime(datetime_str, dt_code) + meas_date = meas_date.replace(tzinfo=dt.timezone.utc) + break + except ValueError: + pass + if meas_date is None: + warn("Extraction of measurement date from NIRX file failed. " + "This can be caused by files saved in certain locales. " + "Please report this as a github issue. " + "The date is being set to January 1st, 2000, " + "instead of {}".format(datetime_str)) + meas_date = dt.datetime(2000, 1, 1, 0, 0, 0, + tzinfo=dt.timezone.utc) + # Extract frequencies of light used by machine fnirs_wavelengths = [int(s) for s in re.findall(r'(\d+)', - hdr['ImagingParameters']['Wavelengths'])] + hdr['ImagingParameters'][ + 'Wavelengths'])] # Extract source-detectors sources = np.asarray([int(s) for s in re.findall(r'(\d+)-\d+:\d+', - hdr['DataStructure']['S-D-Key'])], int) + hdr['DataStructure'][ + 'S-D-Key'])], int) detectors = np.asarray([int(s) for s in re.findall(r'\d+-(\d+):\d+', - hdr['DataStructure']['S-D-Key'])], int) + hdr['DataStructure'] + ['S-D-Key'])], + int) # Determine if short channels are present and on which detectors if 'shortbundles' in hdr['ImagingParameters']: short_det = [int(s) for s in re.findall(r'(\d+)', - hdr['ImagingParameters']['ShortDetIndex'])] + hdr['ImagingParameters']['ShortDetIndex'])] short_det = np.array(short_det, int) else: short_det = [] @@ -150,6 +175,7 @@ def __init__(self, fname, preload=False, verbose=None): # Note: NIRX also records "Study Type", "Experiment History", # "Additional Notes", "Contact Information" and this information # is currently discarded + # NIRStar does not record an id, or handedness by default subject_info = {} names = inf['name'].split() if len(names) > 0: @@ -161,7 +187,6 @@ def __init__(self, fname, preload=False, verbose=None): if len(names) > 2: subject_info['middle_name'] = \ inf['name'].split()[-2].replace("\"", "") - # subject_info['birthday'] = inf['age'] # TODO: not formatted properly subject_info['sex'] = inf['gender'].replace("\"", "") # Recode values if subject_info['sex'] in {'M', 'Male', '1'}: @@ -170,7 +195,9 @@ def __init__(self, fname, preload=False, verbose=None): subject_info['sex'] = FIFF.FIFFV_SUBJ_SEX_FEMALE else: subject_info['sex'] = FIFF.FIFFV_SUBJ_SEX_UNKNOWN - # NIRStar does not record an id, or handedness by default + subject_info['birthday'] = (meas_date.year - int(inf['age']), + meas_date.month, + meas_date.day) # Read information about probe/montage/optodes # A word on terminology used here: @@ -219,10 +246,11 @@ def __init__(self, fname, preload=False, verbose=None): req_ind = req_ind.astype(int) # Generate meaningful channel names - def prepend(list, str): + def prepend(li, str): str += '{0}' - list = [str.format(i) for i in list] - return(list) + li = [str.format(i) for i in li] + return li + snames = prepend(sources[req_ind], 'S') dnames = prepend(detectors[req_ind], '_D') sdnames = [m + str(n) for m, n in zip(snames, dnames)] @@ -235,6 +263,7 @@ def prepend(list, str): samplingrate, ch_types='fnirs_cw_amplitude') info.update(subject_info=subject_info, dig=dig) + info['meas_date'] = meas_date # Store channel, source, and detector locations # The channel location is stored in the first 3 entries of loc. diff --git a/mne/io/nirx/tests/test_nirx.py b/mne/io/nirx/tests/test_nirx.py index 366dbee49da..109ad57bbbc 100644 --- a/mne/io/nirx/tests/test_nirx.py +++ b/mne/io/nirx/tests/test_nirx.py @@ -6,6 +6,7 @@ import os.path as op import shutil import os +import datetime as dt import pytest from numpy.testing import assert_allclose, assert_array_equal @@ -79,6 +80,8 @@ def test_nirx_15_2_short(): # Test data import assert raw._data.shape == (26, 145) assert raw.info['sfreq'] == 12.5 + assert raw.info['meas_date'] == dt.datetime(2019, 8, 23, 7, 37, 4, 540000, + tzinfo=dt.timezone.utc) # Test channel naming assert raw.info['ch_names'][:4] == ["S1_D1 760", "S1_D1 850", @@ -92,7 +95,8 @@ def test_nirx_15_2_short(): # Test info import assert raw.info['subject_info'] == dict(sex=1, first_name="MNE", middle_name="Test", - last_name="Recording") + last_name="Recording", + birthday=(2014, 8, 23)) # Test distance between optodes matches values from # nirsite https://github.com/mne-tools/mne-testing-data/pull/51 @@ -179,8 +183,10 @@ def test_nirx_15_3_short(): assert raw.info['chs'][1]['loc'][9] == 850 # Test info import - assert raw.info['subject_info'] == dict( - sex=0, first_name="testMontage\\0ATestMontage") + assert raw.info['subject_info'] == dict(birthday=(2020, 8, 18), + sex=0, + first_name="testMontage\\0A" + "TestMontage") # Test distance between optodes matches values from # https://github.com/mne-tools/mne-testing-data/pull/72 @@ -257,7 +263,8 @@ def test_encoding(tmpdir): for line in hdr: fid.write(line) # smoke test - read_raw_nirx(fname) + with pytest.raises(RuntimeWarning, match='Extraction of measurement date'): + read_raw_nirx(fname) @requires_testing_data @@ -268,16 +275,20 @@ def test_nirx_15_2(): # Test data import assert raw._data.shape == (64, 67) assert raw.info['sfreq'] == 3.90625 + assert raw.info['meas_date'] == dt.datetime(2019, 10, 2, 9, 8, 47, 511000, + tzinfo=dt.timezone.utc) # Test channel naming assert raw.info['ch_names'][:4] == ["S1_D1 760", "S1_D1 850", "S1_D10 760", "S1_D10 850"] # Test info import - assert raw.info['subject_info'] == dict(sex=1, first_name="TestRecording") + assert raw.info['subject_info'] == dict(sex=1, first_name="TestRecording", + birthday=(1989, 10, 2)) # Test trigger events assert_array_equal(raw.annotations.description, ['4.0', '6.0', '2.0']) + print(raw.annotations.onset) # Test location of detectors allowed_dist_error = 0.0002 @@ -312,6 +323,9 @@ def test_nirx_15_0(): # Test data import assert raw._data.shape == (20, 92) assert raw.info['sfreq'] == 6.25 + assert raw.info['meas_date'] == dt.datetime(2019, 10, 27, 13, 53, 34, + 209000, + tzinfo=dt.timezone.utc) # Test channel naming assert raw.info['ch_names'][:12] == ["S1_D1 760", "S1_D1 850", @@ -322,7 +336,8 @@ def test_nirx_15_0(): "S6_D6 760", "S6_D6 850"] # Test info import - assert raw.info['subject_info'] == {'first_name': 'NIRX', + assert raw.info['subject_info'] == {'birthday': (2004, 10, 27), + 'first_name': 'NIRX', 'last_name': 'Test', 'sex': FIFF.FIFFV_SUBJ_SEX_UNKNOWN}