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

[ENH] Retain sub-second resolution in scans files #451

Merged
merged 9 commits into from
May 28, 2020
36 changes: 18 additions & 18 deletions heudiconv/bids.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,24 @@
json_dumps_pretty,
set_readonly,
is_readonly,
get_datetime,
)

lgr = logging.getLogger(__name__)

# Fields to be populated in _scans files. Order matters
SCANS_FILE_FIELDS = OrderedDict([
("filename", OrderedDict([
("Description", "Name of the nifti file")])),
("acq_time", OrderedDict([
("LongName", "Acquisition time"),
("Description", "Acquisition time of the particular scan")])),
("operator", OrderedDict([
("Description", "Name of the operator")])),
("randstr", OrderedDict([
("LongName", "Random string"),
("Description", "md5 hash of UIDs")])),
])

class BIDSError(Exception):
pass
Expand Down Expand Up @@ -359,22 +373,9 @@ def add_rows_to_scans_keys_file(fn, newrows):
# _scans.tsv). This auto generation will make BIDS-validator happy.
scans_json = '.'.join(fn.split('.')[:-1] + ['json'])
if not op.lexists(scans_json):
save_json(scans_json,
OrderedDict([
("filename", OrderedDict([
("Description", "Name of the nifti file")])),
("acq_time", OrderedDict([
("LongName", "Acquisition time"),
("Description", "Acquisition time of the particular scan")])),
("operator", OrderedDict([
("Description", "Name of the operator")])),
("randstr", OrderedDict([
("LongName", "Random string"),
("Description", "md5 hash of UIDs")])),
]),
sort_keys=False)
save_json(scans_json, SCANS_FILE_FIELDS, sort_keys=False)

header = ['filename', 'acq_time', 'operator', 'randstr']
header = SCANS_FILE_FIELDS
# prepare all the data rows
data_rows = [[k] + v for k, v in fnames2info.items()]
# sort by the date/filename
Expand Down Expand Up @@ -406,9 +407,8 @@ def get_formatted_scans_key_row(dcm_fn):
# parse date and time and get it into isoformat
try:
date = dcm_data.ContentDate
time = dcm_data.ContentTime.split('.')[0]
td = time + date
acq_time = datetime.strptime(td, '%H%M%S%Y%m%d').isoformat()
time = dcm_data.ContentTime
acq_time = get_datetime(date, time)
except (AttributeError, ValueError) as exc:
lgr.warning("Failed to get date/time for the content: %s", str(exc))
acq_time = ''
Expand Down
2 changes: 1 addition & 1 deletion heudiconv/tests/test_heuristics.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ def test_scans_keys_reproin(tmpdir, invocation):
if i != 0:
assert(os.path.exists(pjoin(dirname(scans_keys[0]), row[0])))
assert(re.match(
'^[\d]{4}-[\d]{2}-[\d]{2}T[\d]{2}:[\d]{2}:[\d]{2}$',
'^[\d]{4}-[\d]{2}-[\d]{2}T[\d]{2}:[\d]{2}:[\d]{2}.[\d]{6}$',
row[1]))


Expand Down
2 changes: 1 addition & 1 deletion heudiconv/tests/test_main.py
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ def test_get_formatted_scans_key_row():

row1 = get_formatted_scans_key_row(dcm_fn)
assert len(row1) == 3
assert row1[0] == '2016-10-14T09:26:36'
assert row1[0] == '2016-10-14T09:26:36.693000'
assert row1[1] == 'n/a'
prandstr1 = row1[2]

Expand Down
10 changes: 10 additions & 0 deletions heudiconv/tests/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
load_json,
create_tree,
save_json,
get_datetime,
JSONDecodeError)

import pytest
Expand Down Expand Up @@ -85,3 +86,12 @@ def test_load_json(tmpdir, caplog):
save_json(valid_json_file, vcontent)

assert load_json(valid_json_file) == vcontent


def test_get_datetime():
"""
Test utils.get_datetime()
"""
assert get_datetime('20200512', '162130') == '2020-05-12T16:21:30'
assert get_datetime('20200512', '162130.5') == '2020-05-12T16:21:30.500000'
assert get_datetime('20200512', '162130.5', microseconds=False) == '2020-05-12T16:21:30'
31 changes: 31 additions & 0 deletions heudiconv/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
from collections import namedtuple
from glob import glob
from subprocess import check_output
from datetime import datetime

from nipype.utils.filemanip import which

Expand Down Expand Up @@ -505,3 +506,33 @@ def get_typed_attr(obj, attr, _type, default=None):
except (TypeError, ValueError):
return default
return val


def get_datetime(date, time, *, microseconds=True):
"""
Combine date and time from dicom to isoformat.

Parameters
----------
date : str
Date in YYYYMMDD format.
time : str
Time in either HHMMSS.ffffff format or HHMMSS format.
microseconds: bool, optional
Either to include microseconds in the output

Returns
-------
datetime_str : str
Combined date and time in ISO format, with microseconds as
if fraction was provided in 'time', and 'microseconds' was
True.
"""
if '.' not in time:
# add dummy microseconds if not available for strptime to parse
time += '.000000'
td = time + ':' + date
datetime_str = datetime.strptime(td, '%H%M%S.%f:%Y%m%d').isoformat()
if not microseconds:
datetime_str = datetime_str.split('.', 1)[0]
return datetime_str