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

BUG: Fix bug extracting meas_date #10277

Merged
merged 2 commits into from
Feb 1, 2022
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
2 changes: 2 additions & 0 deletions doc/changes/latest.inc
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,8 @@ Bugs

- :func:`mne.gui.coregistration` now works with surfaces containing topological defects (:gh:`10230`, by `Richard Höchenberger`_)

- Fix bug with :func:`mne.io.read_raw_nirx` being unable to read measurement dates recorded on systems with German (de_DE), French (fr_FR), and Italian (it_IT) locales (:gh:`10277` by `Eric Larson`_)

- :func:`mne.read_trans` now correctly expands ``~`` in the provided path to the user's home directory (:gh:`10265`, by `Richard Höchenberger`_)

API changes
Expand Down
4 changes: 2 additions & 2 deletions mne/datasets/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@
# respective repos, and make a new release of the dataset on GitHub. Then
# update the checksum in the MNE_DATASETS dict below, and change version
# here: ↓↓↓↓↓ ↓↓↓
RELEASES = dict(testing='0.128', misc='0.23')
RELEASES = dict(testing='0.129', misc='0.23')
TESTING_VERSIONED = f'mne-testing-data-{RELEASES["testing"]}'
MISC_VERSIONED = f'mne-misc-data-{RELEASES["misc"]}'

Expand All @@ -111,7 +111,7 @@
# Testing and misc are at the top as they're updated most often
MNE_DATASETS['testing'] = dict(
archive_name=f'{TESTING_VERSIONED}.tar.gz', # 'mne-testing-data',
hash='md5:88c04e31fd496f394fa96fe7cdd70217',
hash='md5:6847ad93bc025d2f553112baa25ad2a6',
url=('https://codeload.github.com/mne-tools/mne-testing-data/'
f'tar.gz/{RELEASES["testing"]}'),
folder_name='MNE-testing-data',
Expand Down
60 changes: 60 additions & 0 deletions mne/io/nirx/_localized_abbr.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
"""Localizations for meas_date extraction."""
# Authors: Eric Larson <larson.eric.d@gmail.com>
#
# License: BSD-3-Clause

# This file was generated on 2021/01/31 on an Ubuntu system.
# When getting "unsupported locale setting" on Ubuntu (e.g., with localepurge),
# use "sudo locale-gen de_DE" etc. then "sudo update-locale".

"""
import datetime
import locale
print('_localized_abbr = {')
for loc in ('en_US.utf8', 'de_DE', 'fr_FR', 'it_IT'):
print(f' {repr(loc)}: {{')
print(' "month": {', end='')
month_abbr = set()
for month in range(1, 13): # Month as locale’s abbreviated name
locale.setlocale(locale.LC_TIME, "en_US.utf8")
dt = datetime.datetime(year=2000, month=month, day=1)
val = dt.strftime("%b").lower()
locale.setlocale(locale.LC_TIME, loc)
key = dt.strftime("%b").lower()
month_abbr.add(key)
print(f'{repr(key)}: {repr(val)}, ', end='')
print('}, # noqa')
print(' "weekday": {', end='')
weekday_abbr = set()
for day in range(1, 8): # Weekday as locale’s abbreviated name.
locale.setlocale(locale.LC_TIME, "en_US.utf8")
dt = datetime.datetime(year=2000, month=1, day=day)
val = dt.strftime("%a").lower()
locale.setlocale(locale.LC_TIME, loc)
key = dt.strftime("%a").lower()
assert key not in weekday_abbr, key
weekday_abbr.add(key)
print(f'{repr(key)}: {repr(val)}, ', end='')
print('}, # noqa')
print(' },')
print('}\n')
"""

_localized_abbr = {
'en_US.utf8': {
"month": {'jan': 'jan', 'feb': 'feb', 'mar': 'mar', 'apr': 'apr', 'may': 'may', 'jun': 'jun', 'jul': 'jul', 'aug': 'aug', 'sep': 'sep', 'oct': 'oct', 'nov': 'nov', 'dec': 'dec', }, # noqa
"weekday": {'sat': 'sat', 'sun': 'sun', 'mon': 'mon', 'tue': 'tue', 'wed': 'wed', 'thu': 'thu', 'fri': 'fri', }, # noqa
},
'de_DE': {
"month": {'jan': 'jan', 'feb': 'feb', 'mär': 'mar', 'apr': 'apr', 'mai': 'may', 'jun': 'jun', 'jul': 'jul', 'aug': 'aug', 'sep': 'sep', 'okt': 'oct', 'nov': 'nov', 'dez': 'dec', }, # noqa
"weekday": {'sa': 'sat', 'so': 'sun', 'mo': 'mon', 'di': 'tue', 'mi': 'wed', 'do': 'thu', 'fr': 'fri', }, # noqa
},
'fr_FR': {
"month": {'janv.': 'jan', 'févr.': 'feb', 'mars': 'mar', 'avril': 'apr', 'mai': 'may', 'juin': 'jun', 'juil.': 'jul', 'août': 'aug', 'sept.': 'sep', 'oct.': 'oct', 'nov.': 'nov', 'déc.': 'dec', }, # noqa
"weekday": {'sam.': 'sat', 'dim.': 'sun', 'lun.': 'mon', 'mar.': 'tue', 'mer.': 'wed', 'jeu.': 'thu', 'ven.': 'fri', }, # noqa
},
'it_IT': {
"month": {'gen': 'jan', 'feb': 'feb', 'mar': 'mar', 'apr': 'apr', 'mag': 'may', 'giu': 'jun', 'lug': 'jul', 'ago': 'aug', 'set': 'sep', 'ott': 'oct', 'nov': 'nov', 'dic': 'dec', }, # noqa
"weekday": {'sab': 'sat', 'dom': 'sun', 'lun': 'mon', 'mar': 'tue', 'mer': 'wed', 'gio': 'thu', 'ven': 'fri', }, # noqa
},
}
43 changes: 33 additions & 10 deletions mne/io/nirx/nirx.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

import numpy as np

from ._localized_abbr import _localized_abbr
from ..base import BaseRaw
from ..utils import _mult_cal_one
from ..constants import FIFF
Expand Down Expand Up @@ -185,21 +186,43 @@ def __init__(self, fname, saturated, preload=False, verbose=None):

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"',
'%Y-%m-%d %H:%M:%S.%f']:
try:
meas_date = dt.datetime.strptime(datetime_str, dt_code)
meas_date = meas_date.replace(tzinfo=dt.timezone.utc)
for loc, translations in _localized_abbr.items():
do_break = False
# So far we are lucky in that all the formats below, if they
# include %a (weekday abbr), always come first. Thus we can use
# a .split(), replace, and rejoin.
loc_datetime_str = datetime_str.split(' ')
for key, val in translations['weekday'].items():
loc_datetime_str[0] = loc_datetime_str[0].replace(key, val)
for ii in range(1, len(loc_datetime_str)):
for key, val in translations['month'].items():
loc_datetime_str[ii] = \
loc_datetime_str[ii].replace(key, val)
loc_datetime_str = ' '.join(loc_datetime_str)
logger.debug(f'Trying {loc} datetime: {loc_datetime_str}')
for dt_code in ['"%a, %b %d, %Y""%H:%M:%S.%f"',
'"%a %d %b %Y""%H:%M:%S.%f"',
'"%a, %d %b %Y""%H:%M:%S.%f"',
'%Y-%m-%d %H:%M:%S.%f']:
try:
meas_date = dt.datetime.strptime(loc_datetime_str, dt_code)
except ValueError:
pass
else:
meas_date = meas_date.replace(tzinfo=dt.timezone.utc)
do_break = True
logger.debug(
f'Measurement date language {loc} detected: {dt_code}')
break
if do_break:
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. "
"This can be caused by files saved in certain locales "
f"(currently only {list(_localized_abbr)} supported). "
"Please report this as a github issue. "
"The date is being set to January 1st, 2000, "
"instead of {}".format(datetime_str))
f"instead of {repr(datetime_str)}.")
meas_date = dt.datetime(2000, 1, 1, 0, 0, 0,
tzinfo=dt.timezone.utc)

Expand Down
72 changes: 44 additions & 28 deletions mne/io/nirx/tests/test_nirx.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,40 +23,45 @@
short_channels
from mne.io.constants import FIFF

fname_nirx_15_0 = op.join(data_path(download=False),
'NIRx', 'nirscout', 'nirx_15_0_recording')
fname_nirx_15_2 = op.join(data_path(download=False),
'NIRx', 'nirscout', 'nirx_15_2_recording')
fname_nirx_15_2_short = op.join(data_path(download=False),
'NIRx', 'nirscout',
'nirx_15_2_recording_w_short')
fname_nirx_15_3_short = op.join(data_path(download=False),
'NIRx', 'nirscout', 'nirx_15_3_recording')
testing_path = data_path(download=False)
fname_nirx_15_0 = op.join(
testing_path, 'NIRx', 'nirscout', 'nirx_15_0_recording')
fname_nirx_15_2 = op.join(
testing_path, 'NIRx', 'nirscout', 'nirx_15_2_recording')
fname_nirx_15_2_short = op.join(
testing_path, 'NIRx', 'nirscout', 'nirx_15_2_recording_w_short')
fname_nirx_15_3_short = op.join(
testing_path, 'NIRx', 'nirscout', 'nirx_15_3_recording')


# This file has no saturated sections
nirsport1_wo_sat = op.join(data_path(download=False), 'NIRx', 'nirsport_v1',
nirsport1_wo_sat = op.join(testing_path, 'NIRx', 'nirsport_v1',
'nirx_15_3_recording_wo_saturation')
# This file has saturation, but not on the optode pairing in montage
nirsport1_w_sat = op.join(data_path(download=False), 'NIRx', 'nirsport_v1',
nirsport1_w_sat = op.join(testing_path, 'NIRx', 'nirsport_v1',
'nirx_15_3_recording_w_saturation_'
'not_on_montage_channels')
# This file has saturation in channels of interest
nirsport1_w_fullsat = op.join(data_path(download=False), 'NIRx', 'nirsport_v1',
'nirx_15_3_recording_w_'
'saturation_on_montage_channels')
nirsport1_w_fullsat = op.join(
testing_path, 'NIRx', 'nirsport_v1', 'nirx_15_3_recording_w_'
'saturation_on_montage_channels')

# NIRSport2 device using Aurora software and matching snirf file
nirsport2 = op.join(data_path(download=False), 'NIRx', 'nirsport_v2',
'aurora_recording _w_short_and_acc')
nirsport2_snirf = op.join(data_path(download=False), 'SNIRF', 'NIRx',
'NIRSport2', '1.0.3', '2021-05-05_001.snirf')
nirsport2 = op.join(
testing_path, 'NIRx', 'nirsport_v2', 'aurora_recording _w_short_and_acc')
nirsport2_snirf = op.join(
testing_path, 'SNIRF', 'NIRx', 'NIRSport2', '1.0.3',
'2021-05-05_001.snirf')

nirsport2_2021_9 = op.join(data_path(download=False), 'NIRx', 'nirsport_v2',
'aurora_2021_9')
snirf_nirsport2_20219 = op.join(data_path(download=False),
'SNIRF', 'NIRx', 'NIRSport2', '2021.9',
'2021-10-01_002.snirf')
nirsport2_2021_9 = op.join(
testing_path, 'NIRx', 'nirsport_v2', 'aurora_2021_9')
snirf_nirsport2_20219 = op.join(
testing_path, 'SNIRF', 'NIRx', 'NIRSport2', '2021.9',
'2021-10-01_002.snirf')

# NIRStar (with Italian locale)
nirstar_it = op.join(
testing_path, 'NIRx', 'nirstar', '2020-01-24_SHAM_CTRL_0050')


@requires_h5py
Expand Down Expand Up @@ -456,21 +461,31 @@ def test_nirx_15_3_short():


@requires_testing_data
def test_encoding(tmp_path):
def test_locale_encoding(tmp_path):
"""Test NIRx encoding."""
fname = tmp_path / 'latin'
shutil.copytree(fname_nirx_15_2, fname)
hdr_fname = op.join(fname, 'NIRS-2019-10-02_003.hdr')
hdr = list()
with open(hdr_fname, 'rb') as fid:
hdr.extend(line for line in fid)
# French
hdr[2] = b'Date="jeu. 13 f\xe9vr. 2020"\r\n'
with open(hdr_fname, 'wb') as fid:
for line in hdr:
fid.write(line)
# smoke test
with pytest.raises(RuntimeWarning, match='Extraction of measurement date'):
read_raw_nirx(fname)
read_raw_nirx(fname, verbose='debug')
# German
hdr[2] = b'Date="mi 13 dez 2020"\r\n'
with open(hdr_fname, 'wb') as fid:
for line in hdr:
fid.write(line)
read_raw_nirx(fname, verbose='debug')
# Italian
raw = read_raw_nirx(nirstar_it, verbose='debug')
want_dt = dt.datetime(
2020, 1, 24, 10, 57, 41, 454000, tzinfo=dt.timezone.utc)
assert raw.info['meas_date'] == want_dt


@requires_testing_data
Expand Down Expand Up @@ -577,7 +592,8 @@ def test_nirx_15_0():
[fname_nirx_15_2_short, 1],
[fname_nirx_15_2, 0],
[fname_nirx_15_2, 0],
[nirsport2_2021_9, 0]
[nirsport2_2021_9, 0],
[nirstar_it, 0],
))
def test_nirx_standard(fname, boundary_decimal):
"""Test standard operations."""
Expand Down