Skip to content

Commit

Permalink
BUG: Fix bug extracting meas_date (#10277)
Browse files Browse the repository at this point in the history
* BUG: Fix bug extracting meas_date

* STY: pydocstyle
  • Loading branch information
larsoner authored Feb 1, 2022
1 parent e11f1fc commit d53c5bb
Show file tree
Hide file tree
Showing 5 changed files with 141 additions and 40 deletions.
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

0 comments on commit d53c5bb

Please sign in to comment.