Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

MRG: Extract measurement date and age for NIRX files #7891

Merged
merged 8 commits into from
Sep 9, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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