Skip to content

Commit d56ecf0

Browse files
committed
ENH(+RF): add acq_time_precise after acq_time to _scans
Unfortunately BIDS ATM does not instruct possibility to add .microseconds to acq_time within _scans files, explicitly stops on "seconds". So to not break existing tools supporting BIDS and stay compliant, to introduce sub-second precision we are adding an ad-hoc field (since allowed). Note: in this commit I have not taken advantage of prior commit which introduced microseconds keyword argument to get_datetime - to not waste cpu cycles on reparsing and re-conversion. while at it also centralized definition of which column headers to expect.
1 parent d3e247c commit d56ecf0

File tree

3 files changed

+36
-30
lines changed

3 files changed

+36
-30
lines changed

heudiconv/bids.py

Lines changed: 21 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,23 @@
2626

2727
lgr = logging.getLogger(__name__)
2828

29+
# Fields to be populated in _scans files. Order matters
30+
SCANS_FILE_FIELDS = OrderedDict([
31+
("filename", OrderedDict([
32+
("Description", "Name of the nifti file")])),
33+
("acq_time", OrderedDict([
34+
("LongName", "Acquisition time"),
35+
("Description", "Acquisition time of the particular scan")])),
36+
("acq_time_precise", OrderedDict([
37+
("LongName", "Acquisition time precise"),
38+
("Description", "Acquisition time of the particular scan "
39+
"(with microseconds if available)")])),
40+
("operator", OrderedDict([
41+
("Description", "Name of the operator")])),
42+
("randstr", OrderedDict([
43+
("LongName", "Random string"),
44+
("Description", "md5 hash of UIDs")])),
45+
])
2946

3047
class BIDSError(Exception):
3148
pass
@@ -360,22 +377,9 @@ def add_rows_to_scans_keys_file(fn, newrows):
360377
# _scans.tsv). This auto generation will make BIDS-validator happy.
361378
scans_json = '.'.join(fn.split('.')[:-1] + ['json'])
362379
if not op.lexists(scans_json):
363-
save_json(scans_json,
364-
OrderedDict([
365-
("filename", OrderedDict([
366-
("Description", "Name of the nifti file")])),
367-
("acq_time", OrderedDict([
368-
("LongName", "Acquisition time"),
369-
("Description", "Acquisition time of the particular scan")])),
370-
("operator", OrderedDict([
371-
("Description", "Name of the operator")])),
372-
("randstr", OrderedDict([
373-
("LongName", "Random string"),
374-
("Description", "md5 hash of UIDs")])),
375-
]),
376-
sort_keys=False)
380+
save_json(scans_json, SCANS_FILE_FIELDS, sort_keys=False)
377381

378-
header = ['filename', 'acq_time', 'operator', 'randstr']
382+
header = SCANS_FILE_FIELDS
379383
# prepare all the data rows
380384
data_rows = [[k] + v for k, v in fnames2info.items()]
381385
# sort by the date/filename
@@ -399,7 +403,7 @@ def get_formatted_scans_key_row(dcm_fn):
399403
Returns
400404
-------
401405
row: list
402-
[ISO acquisition time, performing physician name, random string]
406+
[date time, date time (with microseconds), performing physician name, random string]
403407
404408
"""
405409
dcm_data = dcm.read_file(dcm_fn, stop_before_pixels=True, force=True)
@@ -424,7 +428,7 @@ def get_formatted_scans_key_row(dcm_fn):
424428
perfphys = dcm_data.PerformingPhysicianName
425429
except AttributeError:
426430
perfphys = ''
427-
row = [acq_time, perfphys, randstr]
431+
row = [acq_time.split('.')[0], acq_time, perfphys, randstr]
428432
# empty entries should be 'n/a'
429433
# https://github.com/dartmouth-pbs/heudiconv/issues/32
430434
row = ['n/a' if not str(e) else e for e in row]

heudiconv/tests/test_heuristics.py

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -111,13 +111,15 @@ def test_scans_keys_reproin(tmpdir, invocation):
111111
reader = csv.reader(f, delimiter='\t')
112112
for i, row in enumerate(reader):
113113
if i == 0:
114-
assert(row == ['filename', 'acq_time', 'operator', 'randstr'])
115-
assert(len(row) == 4)
114+
assert(row == ['filename', 'acq_time', 'acq_time_precise', 'operator', 'randstr'])
115+
assert(len(row) == 5)
116116
if i != 0:
117117
assert(os.path.exists(pjoin(dirname(scans_keys[0]), row[0])))
118-
assert(re.match(
119-
'^[\d]{4}-[\d]{2}-[\d]{2}T[\d]{2}:[\d]{2}:[\d]{2}.[\d]{6}$',
120-
row[1]))
118+
bidsdatetime_regex = '^[\d]{4}-[\d]{2}-[\d]{2}T[\d]{2}:[\d]{2}:[\d]{2}$'
119+
# regular acq_time without .microseconds since BIDS did not envision it
120+
assert(re.match(bidsdatetime_regex, row[1]))
121+
# acq_time_precise
122+
assert(re.match(bidsdatetime_regex.replace("$", ".[\d]{6}$"), row[2]))
121123

122124

123125
@patch('sys.stdout', new_callable=StringIO)

heudiconv/tests/test_main.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,6 @@ def test_prepare_for_datalad(tmpdir):
115115

116116
from datalad.api import Dataset
117117
superds = Dataset(str(tmpdir))
118-
119118
assert superds.is_installed()
120119
assert not superds.repo.dirty
121120
subdss = superds.subdatasets(recursive=True, result_xfm='relpaths')
@@ -172,14 +171,15 @@ def test_get_formatted_scans_key_row():
172171
% TESTS_DATA_PATH
173172

174173
row1 = get_formatted_scans_key_row(dcm_fn)
175-
assert len(row1) == 3
176-
assert row1[0] == '2016-10-14T09:26:36.693000'
177-
assert row1[1] == 'n/a'
178-
prandstr1 = row1[2]
174+
assert len(row1) == 4
175+
assert row1[0] == '2016-10-14T09:26:36'
176+
assert row1[1] == '2016-10-14T09:26:36.693000'
177+
assert row1[2] == 'n/a'
178+
prandstr1 = row1[3]
179179

180180
# if we rerun - should be identical!
181181
row2 = get_formatted_scans_key_row(dcm_fn)
182-
prandstr2 = row2[2]
182+
prandstr2 = row2[3]
183183
assert(prandstr1 == prandstr2)
184184
assert(row1 == row2)
185185
# So it is consistent across pythons etc, we use explicit value here
@@ -189,7 +189,7 @@ def test_get_formatted_scans_key_row():
189189
row3 = get_formatted_scans_key_row(
190190
"%s/01-anat-scout/0001.dcm" % TESTS_DATA_PATH)
191191
assert(row3 != row1)
192-
prandstr3 = row3[2]
192+
prandstr3 = row3[3]
193193
assert(prandstr1 != prandstr3)
194194
assert(prandstr3 == "fae3befb")
195195

@@ -211,7 +211,7 @@ def _check_rows(fn, rows):
211211
rows_loaded.append(row)
212212
for i, row_ in enumerate(rows_loaded):
213213
if i == 0:
214-
assert(row_ == ['filename', 'acq_time', 'operator', 'randstr'])
214+
assert(row_ == ['filename', 'acq_time', 'acq_time_precise', 'operator', 'randstr'])
215215
else:
216216
assert(rows[row_[0]] == row_[1:])
217217
# dates, filename should be sorted (date "first", filename "second")

0 commit comments

Comments
 (0)