Skip to content

Commit 35e95b6

Browse files
authored
Merge pull request #129 from dartmouth-pbs/merge-enh-dbic2
Fixes from DBIC field-test site and code rodeo coding session (AKA reaching convergence)
2 parents e9542e0 + f4ac1c8 commit 35e95b6

File tree

16 files changed

+243
-82
lines changed

16 files changed

+243
-82
lines changed

.travis.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ install:
3636
- git config --global user.name "Travis Almighty"
3737

3838
script:
39-
- coverage run `which py.test` -s -v tests heuristics
39+
- coverage run `which py.test` -s -v tests heuristics/*.py
4040

4141
after_success:
4242
- codecov

heudiconv/__init__.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
# set logger handler
22
import logging
3-
logging.getLogger(__name__).addHandler(logging.NullHandler())
4-
3+
import os
54
from .info import (__version__, __packagename__)
5+
6+
# Rudimentary logging support.
7+
lgr = logging.getLogger(__name__)
8+
logging.basicConfig(
9+
format='%(levelname)s: %(message)s',
10+
level=getattr(logging, os.environ.get('HEUDICONV_LOG_LEVEL', 'INFO'))
11+
)
12+
lgr.debug("Starting the abomination") # just to "run-test" logging

heudiconv/bids.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -61,10 +61,10 @@ def populate_bids_templates(path, defaults={}):
6161
create_file_if_missing(op.join(path, 'CHANGES'),
6262
"0.0.1 Initial data acquired\n"
6363
"TODOs:\n\t- verify and possibly extend information in participants.tsv"
64-
"(see for example http://datasets.datalad.org/?dir=/openfmri/ds000208)"
65-
"\n\t- fill out dataset_description.json, README, sourcedata/README "
66-
"(if present)\n\t- provide _events.tsv file for each _bold.nii.gz with "
67-
"onsets of events (see '8.5 Task events' of BIDS specification)")
64+
" (see for example http://datasets.datalad.org/?dir=/openfmri/ds000208)"
65+
"\n\t- fill out dataset_description.json, README, sourcedata/README"
66+
" (if present)\n\t- provide _events.tsv file for each _bold.nii.gz with"
67+
" onsets of events (see '8.5 Task events' of BIDS specification)")
6868
create_file_if_missing(op.join(path, 'README'),
6969
"TODO: Provide description for the dataset -- basic details about the "
7070
"study, possibly pointing to pre-registration (if public or embargoed)")
@@ -92,7 +92,7 @@ def populate_bids_templates(path, defaults={}):
9292
events_file = fpath[:-len(suf)] + '_events.tsv'
9393
lgr.debug("Generating %s", events_file)
9494
with open(events_file, 'w') as f:
95-
f.write("onset\tduration\ttrial_type\tresponse_time\tTODO -- fill in rows and add more tab-separated columns if desired")
95+
f.write("onset\tduration\ttrial_type\tresponse_time\tstim_file\tTODO -- fill in rows and add more tab-separated columns if desired")
9696
# extract tasks files stubs
9797
for task_acq, fields in tasks.items():
9898
task_file = op.join(path, task_acq + '_bold.json')

heudiconv/cli/run.py

Lines changed: 26 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from ..utils import load_heuristic, anonymize_sid, treat_infofile, SeqInfo
99
from ..convert import prep_conversion
1010

11+
from ..bids import (populate_bids_templates, tuneup_bids_json_files)
1112
import inspect
1213
import logging
1314
lgr = logging.getLogger(__name__)
@@ -87,11 +88,20 @@ def process_extra_commands(outdir, args):
8788
def main(argv=None):
8889
parser = get_parser()
8990
args = parser.parse_args(argv)
91+
# To be done asap so anything random is deterministic
92+
if args.random_seed is not None:
93+
import random
94+
random.seed(args.random_seed)
95+
import numpy
96+
numpy.random.seed(args.random_seed)
9097
if args.debug:
9198
lgr.setLevel(logging.DEBUG)
92-
93-
if args.files and args.subjs:
94-
raise ValueError("Unable to processes `--subjects` with files")
99+
# Should be possible but only with a single subject -- will be used to
100+
# override subject deduced from the DICOMs
101+
if args.files and args.subjs and len(args.subjs) > 1:
102+
raise ValueError(
103+
"Unable to processes multiple `--subjects` with files"
104+
)
95105

96106
if args.debug:
97107
setup_exceptionhook()
@@ -119,9 +129,10 @@ def get_parser():
119129
help='list of subjects - required for dicom template. '
120130
'If not provided, DICOMS would first be "sorted" and '
121131
'subject IDs deduced by the heuristic')
122-
parser.add_argument('-c', '--converter', default='dcm2niix',
132+
parser.add_argument('-c', '--converter',
133+
default='dcm2niix',
123134
choices=('dcm2niix', 'none'),
124-
help='tool to use for dicom conversion. Setting to '
135+
help='tool to use for DICOM conversion. Setting to '
125136
'"none" disables the actual conversion step -- useful'
126137
'for testing heuristics.')
127138
parser.add_argument('-o', '--outdir', default=os.getcwd(),
@@ -149,7 +160,7 @@ def get_parser():
149160
parser.add_argument('-b', '--bids', action='store_true',
150161
help='flag for output into BIDS structure')
151162
parser.add_argument('--overwrite', action='store_true', default=False,
152-
help='flag to allow overwrite existing files')
163+
help='flag to allow overwriting existing converted files')
153164
parser.add_argument('--datalad', action='store_true',
154165
help='Store the entire collection as DataLad '
155166
'dataset(s). Small files will be committed directly to '
@@ -171,7 +182,8 @@ def get_parser():
171182
parser.add_argument('--minmeta', action='store_true',
172183
help='Exclude dcmstack meta information in sidecar '
173184
'jsons')
174-
185+
parser.add_argument('--random-seed', type=int, default=None,
186+
help='Random seed to initialize RNG')
175187
submission = parser.add_argument_group('Conversion submission options')
176188
submission.add_argument('-q', '--queue', default=None,
177189
help='select batch system to submit jobs to instead'
@@ -215,6 +227,9 @@ def process_args(args):
215227

216228
for (locator, session, sid), files_or_seqinfo in study_sessions.items():
217229

230+
# Allow for session to be overloaded from command line
231+
if args.session is not None:
232+
session = args.session
218233
if not len(files_or_seqinfo):
219234
raise ValueError("nothing to process?")
220235
# that is how life is ATM :-/ since we don't do sorting if subj
@@ -250,7 +265,7 @@ def process_args(args):
250265
sid,
251266
args.anon_cmd,
252267
args.converter,
253-
args.session,
268+
session,
254269
args.with_prov,
255270
args.bids)
256271
continue
@@ -269,10 +284,10 @@ def process_args(args):
269284
from ..external.dlad import prepare_datalad
270285
dlad_sid = sid if not anon_sid else anon_sid
271286
dl_msg = prepare_datalad(anon_study_outdir, anon_outdir, dlad_sid,
272-
args.session, seqinfo, dicoms, args.bids)
287+
session, seqinfo, dicoms, args.bids)
273288

274289
lgr.info("PROCESSING STARTS: {0}".format(
275-
str(dict(subject=sid, outdir=study_outdir, session=args.session))))
290+
str(dict(subject=sid, outdir=study_outdir, session=session))))
276291

277292
prep_conversion(sid,
278293
dicoms,
@@ -282,7 +297,7 @@ def process_args(args):
282297
anon_sid=anon_sid,
283298
anon_outdir=anon_study_outdir,
284299
with_prov=args.with_prov,
285-
ses=args.session,
300+
ses=session,
286301
bids=args.bids,
287302
seqinfo=seqinfo,
288303
min_meta=args.minmeta,

heudiconv/convert.py

Lines changed: 51 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414
set_readonly,
1515
clear_temp_dicoms,
1616
seqinfo_fields,
17+
assure_no_file_exists,
18+
file_md5sum
1719
)
1820
from .bids import (
1921
convert_sid_bids,
@@ -100,15 +102,35 @@ def prep_conversion(sid, dicoms, outdir, heuristic, converter, anon_sid,
100102
if not op.exists(idir):
101103
os.makedirs(idir)
102104

103-
shutil.copy(heuristic.filename, idir)
104105
ses_suffix = "_ses-%s" % ses if ses is not None else ""
105106
info_file = op.join(idir, '%s%s.auto.txt' % (sid, ses_suffix))
106107
edit_file = op.join(idir, '%s%s.edit.txt' % (sid, ses_suffix))
107108
filegroup_file = op.join(idir, 'filegroup%s.json' % ses_suffix)
108109

110+
# if conversion table(s) do not exist -- we need to prepare them
111+
# (the *prepare* stage in https://github.com/nipy/heudiconv/issues/134)
112+
reuse_conversion_table = op.exists(edit_file)
113+
# We also might need to redo it if changes in the heuristic file
114+
# detected
115+
# ref: https://github.com/nipy/heudiconv/issues/84#issuecomment-330048609
116+
# for more automagical wishes
117+
target_heuristic_filename = op.join(idir, op.basename(heuristic.filename))
118+
# TODO:
119+
# 1. add a test
120+
# 2. possibly extract into a dedicated function for easier logic flow here
121+
# and a dedicated unittest
122+
if not reuse_conversion_table and \
123+
op.exists(target_heuristic_filename) and \
124+
file_md5sum(target_heuristic_filename) != file_md5sum(heuristic.filename):
125+
reuse_conversion_table = False
126+
lgr.info(
127+
"Will not reuse existing conversion table files because heuristic "
128+
"has changed"
129+
)
130+
109131
# MG - maybe add an option to force rerun?
110132
# related issue : https://github.com/nipy/heudiconv/issues/84
111-
if op.exists(edit_file) and overwrite:
133+
if reuse_conversion_table:
112134
lgr.info("Reloading existing filegroup.json "
113135
"because %s exists", edit_file)
114136
info = read_config(edit_file)
@@ -122,6 +144,8 @@ def prep_conversion(sid, dicoms, outdir, heuristic, converter, anon_sid,
122144
else:
123145
# TODO -- might have been done outside already!
124146
# MG -- will have to try with both dicom template, files
147+
assure_no_file_exists(target_heuristic_filename)
148+
safe_copyfile(heuristic.filename, idir)
125149
if dicoms:
126150
seqinfo = group_dicoms_into_seqinfos(
127151
dicoms,
@@ -131,6 +155,8 @@ def prep_conversion(sid, dicoms, outdir, heuristic, converter, anon_sid,
131155
seqinfo_list = list(seqinfo.keys())
132156
filegroup = {si.series_id: x for si, x in seqinfo.items()}
133157
dicominfo_file = op.join(idir, 'dicominfo%s.tsv' % ses_suffix)
158+
# allow to overwrite even if was present under git-annex already
159+
assure_no_file_exists(dicominfo_file)
134160
with open(dicominfo_file, 'wt') as fp:
135161
fp.write('\t'.join([val for val in seqinfo_fields]) + '\n')
136162
for seq in seqinfo_list:
@@ -139,7 +165,9 @@ def prep_conversion(sid, dicoms, outdir, heuristic, converter, anon_sid,
139165
info = heuristic.infotodict(seqinfo_list)
140166
lgr.debug("Writing to {}, {}, {}".format(info_file, edit_file,
141167
filegroup_file))
168+
assure_no_file_exists(info_file)
142169
write_config(info_file, info)
170+
assure_no_file_exists(edit_file)
143171
write_config(edit_file, info)
144172
save_json(filegroup_file, filegroup)
145173

@@ -150,7 +178,7 @@ def prep_conversion(sid, dicoms, outdir, heuristic, converter, anon_sid,
150178
else:
151179
tdir = op.join(anon_outdir, anon_sid)
152180

153-
if converter != 'none':
181+
if converter.lower() != 'none':
154182
lgr.info("Doing conversion using %s", converter)
155183
cinfo = conversion_info(anon_sid, tdir, info, filegroup, ses)
156184
convert(cinfo,
@@ -218,8 +246,8 @@ def convert(items, converter, scaninfo_suffix, custom_callable, with_prov,
218246
os.makedirs(prefix_dirname)
219247

220248
for outtype in outtypes:
221-
lgr.debug("Processing %d dicoms for output type %s",
222-
len(item_dicoms), outtype)
249+
lgr.debug("Processing %d dicoms for output type %s. Overwrite=%s",
250+
len(item_dicoms), outtype, overwrite)
223251
lgr.debug("Includes the following dicoms: %s", item_dicoms)
224252

225253
seqtype = op.basename(op.dirname(prefix)) if bids else None
@@ -243,7 +271,8 @@ def convert(items, converter, scaninfo_suffix, custom_callable, with_prov,
243271

244272
bids_outfiles = save_converted_files(res, item_dicoms, bids,
245273
outtype, prefix,
246-
outname_bids)
274+
outname_bids,
275+
overwrite=overwrite)
247276

248277
# save acquisition time information if it's BIDS
249278
# at this point we still have acquisition date
@@ -257,15 +286,23 @@ def convert(items, converter, scaninfo_suffix, custom_callable, with_prov,
257286
safe_copyfile(op.join(convertnode.base_dir,
258287
convertnode.name,
259288
'provenance.ttl'),
260-
prov_file)
289+
prov_file,
290+
overwrite=overwrite)
261291
prov_files.append(prov_file)
262292

263293
tempdirs.rmtree(tmpdir)
294+
else:
295+
raise RuntimeError(
296+
"was asked to convert into %s but destination already exists"
297+
% (outname)
298+
)
264299

265300
if len(bids_outfiles) > 1:
266301
lgr.warning("For now not embedding BIDS and info generated "
267302
".nii.gz itself since sequence produced "
268303
"multiple files")
304+
elif not bids_outfiles:
305+
lgr.debug("No BIDS files were produced, nothing to embed to then")
269306
else:
270307
embed_metadata_from_dicoms(bids, item_dicoms, outname, outname_bids,
271308
prov_file, scaninfo, tempdirs, with_prov,
@@ -350,7 +387,7 @@ def nipype_convert(item_dicoms, prefix, with_prov, bids, tmpdir):
350387
return convertnode.run()
351388

352389

353-
def save_converted_files(res, item_dicoms, bids, outtype, prefix, outname_bids):
390+
def save_converted_files(res, item_dicoms, bids, outtype, prefix, outname_bids, overwrite):
354391
"""Copy converted files from tempdir to output directory.
355392
Will rename files if necessary.
356393
@@ -381,8 +418,8 @@ def save_converted_files(res, item_dicoms, bids, outtype, prefix, outname_bids):
381418

382419
if isdefined(res.outputs.bvecs) and isdefined(res.outputs.bvals):
383420
outname_bvecs, outname_bvals = prefix + '.bvec', prefix + '.bval'
384-
safe_copyfile(res.outputs.bvecs, outname_bvecs)
385-
safe_copyfile(res.outputs.bvals, outname_bvals)
421+
safe_copyfile(res.outputs.bvecs, outname_bvecs, overwrite)
422+
safe_copyfile(res.outputs.bvals, outname_bvals, overwrite)
386423

387424
if isinstance(res_files, list):
388425
# we should provide specific handling for fmap,
@@ -406,18 +443,18 @@ def save_converted_files(res, item_dicoms, bids, outtype, prefix, outname_bids):
406443

407444
for fl, suffix, bids_file in zip(res_files, suffixes, bids_files):
408445
outname = "%s%s.%s" % (prefix, suffix, outtype)
409-
safe_copyfile(fl, outname)
446+
safe_copyfile(fl, outname, overwrite)
410447
if bids_file:
411448
outname_bids_file = "%s%s.json" % (prefix, suffix)
412-
safe_copyfile(bids_file, outname_bids_file)
449+
safe_copyfile(bids_file, outname_bids_file, overwrite)
413450
bids_outfiles.append(outname_bids_file)
414451
# res_files is not a list
415452
else:
416453
outname = "{}.{}".format(prefix, outtype)
417-
safe_copyfile(res_files, outname)
454+
safe_copyfile(res_files, outname, overwrite)
418455
if isdefined(res.outputs.bids):
419456
try:
420-
safe_copyfile(res.outputs.bids, outname_bids)
457+
safe_copyfile(res.outputs.bids, outname_bids, overwrite)
421458
bids_outfiles.append(outname_bids)
422459
except TypeError as exc: ##catch lists
423460
raise TypeError("Multiple BIDS sidecars detected.")

heudiconv/dicoms.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -185,8 +185,7 @@ def group_dicoms_into_seqinfos(files, file_filter, dcmfilter, grouping):
185185
except AttributeError:
186186
series_desc = ''
187187

188-
motion_corrected = ('moco' in dcminfo.SeriesDescription.lower()
189-
or 'MOCO' in image_type)
188+
motion_corrected = 'MOCO' in image_type
190189

191190
if dcminfo.get([0x18,0x24], None):
192191
# GE and Philips scanners
@@ -448,6 +447,9 @@ def embed_metadata_from_dicoms(bids, item_dicoms, outname, outname_bids,
448447
from nipype import Node, Function
449448
tmpdir = tempdirs(prefix='embedmeta')
450449

450+
# We need to assure that paths are absolute if they are relative
451+
item_dicoms = list(map(op.abspath, item_dicoms))
452+
451453
embedfunc = Node(Function(input_names=['dcmfiles', 'niftifile', 'infofile',
452454
'bids_info', 'force', 'min_meta'],
453455
output_names=['outfile', 'meta'],
@@ -464,6 +466,8 @@ def embed_metadata_from_dicoms(bids, item_dicoms, outname, outname_bids,
464466
embedfunc.inputs.force = True
465467
embedfunc.base_dir = tmpdir
466468
cwd = os.getcwd()
469+
lgr.debug("Embedding into %s based on dicoms[0]=%s for nifti %s",
470+
scaninfo, item_dicoms[0], outname)
467471
try:
468472
if op.lexists(scaninfo):
469473
# TODO: handle annexed file case

heudiconv/external/dlad.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ def add_to_datalad(topdir, studydir, msg, bids):
5151
ds_ = create(curdir_, dataset=superds,
5252
force=True,
5353
no_annex=True,
54-
shared_access='all',
54+
# shared_access='all',
5555
annex_version=6)
5656
assert ds == ds_
5757
assert ds.is_installed()
@@ -94,8 +94,9 @@ def add_to_datalad(topdir, studydir, msg, bids):
9494
"yet provided", ds)
9595
else:
9696
dsh = ds.create(path='.heudiconv',
97-
force=True,
98-
shared_access='all')
97+
force=True
98+
# shared_access='all'
99+
)
99100
# Since .heudiconv could contain sensitive information
100101
# we place all files under annex and then add
101102
if create_file_if_missing(op.join(dsh_path, '.gitattributes'),

heudiconv/info.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,4 +27,5 @@
2727
'datalad': ['datalad']
2828
}
2929

30-
EXTRA_REQUIRES['all'] = list(EXTRA_REQUIRES.values())
30+
# Flatten the lists
31+
EXTRA_REQUIRES['all'] = sum(EXTRA_REQUIRES.values(), [])

0 commit comments

Comments
 (0)