Skip to content

Commit

Permalink
MRG: Extract measurement date and age for NIRX files (mne-tools#7891)
Browse files Browse the repository at this point in the history
* Add meas_date and age to nirx io

* Remove unused import

* Update mne/io/nirx/nirx.py

Co-authored-by: Alexandre Gramfort <[email protected]>

* Update mne/io/nirx/nirx.py

Co-authored-by: Alexandre Gramfort <[email protected]>

* Get date for loop to work

* Shorten tests

* Dont use the word list

* Flake fix

Co-authored-by: Alexandre Gramfort <[email protected]>
  • Loading branch information
2 people authored and marsipu committed Oct 14, 2020
1 parent 0570957 commit c968f4c
Show file tree
Hide file tree
Showing 2 changed files with 61 additions and 17 deletions.
51 changes: 40 additions & 11 deletions mne/io/nirx/nirx.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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
Expand Down Expand Up @@ -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 = []
Expand All @@ -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:
Expand All @@ -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'}:
Expand All @@ -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:
Expand Down Expand Up @@ -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)]
Expand All @@ -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.
Expand Down
27 changes: 21 additions & 6 deletions mne/io/nirx/tests/test_nirx.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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",
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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",
Expand All @@ -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}

Expand Down

0 comments on commit c968f4c

Please sign in to comment.