Skip to content

Commit 121c4ad

Browse files
committed
Merge branch 'master' of github.com:nipy/heudiconv into feature/bids_filelock
2 parents ed146e5 + dcc590d commit 121c4ad

15 files changed

+332
-84
lines changed

.travis.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@
22
language: python
33
python:
44
- 2.7
5-
- 3.4
65
- 3.5
76
- 3.6
7+
- 3.7
88

99
cache:
1010
- apt

docs/usage.rst

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,13 @@ DICOMs as an independent ``heudiconv`` execution.
4343
The first script aggregates the DICOM directories and submits them to
4444
``run_heudiconv.sh`` with SLURM as a job array.
4545

46+
If using bids, the ``notop`` bids option suppresses creation of
47+
top-level files in the bids directory (e.g.,
48+
``dataset_description.json``) to avoid possible race conditions.
49+
These files may be generated later with ``populate_templates.sh``
50+
below (except for ``participants.tsv``, which must be create
51+
manually).
52+
4653
.. code:: shell
4754
4855
#!/bin/bash
@@ -76,7 +83,22 @@ The second script processes a DICOM directory with ``heudiconv`` using the built
7683
echo Submitted directory: ${DCMDIR}
7784
7885
IMG="/singularity-images/heudiconv-0.5.4-dev.sif"
79-
CMD="singularity run -B ${DCMDIR}:/dicoms:ro -B ${OUTDIR}:/output -e ${IMG} --files /dicoms/ -o /output -f reproin -c dcm2niix -b --minmeta -l ."
86+
CMD="singularity run -B ${DCMDIR}:/dicoms:ro -B ${OUTDIR}:/output -e ${IMG} --files /dicoms/ -o /output -f reproin -c dcm2niix -b notop --minmeta -l ."
87+
88+
printf "Command:\n${CMD}\n"
89+
${CMD}
90+
echo "Successful process"
91+
92+
This script creates the top-level bids files (e.g.,
93+
``dataset_description.json``)
94+
95+
..code:: shell
96+
#!/bin/bash
97+
set -eu
98+
99+
OUTDIR=${1}
100+
IMG="/singularity-images/heudiconv-0.5.4-dev.sif"
101+
CMD="singularity run -B ${OUTDIR}:/output -e ${IMG} --files /output -f reproin --command populate-templates"
80102

81103
printf "Command:\n${CMD}\n"
82104
${CMD}

heudiconv/bids.py

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,27 @@ def add_participant_record(studydir, subject, age, sex):
240240
known_subjects = {l.split('\t')[0] for l in f.readlines()}
241241
if participant_id in known_subjects:
242242
return
243+
else:
244+
# Populate particpants.json (an optional file to describe column names in
245+
# participant.tsv). This auto generation will make BIDS-validator happy.
246+
participants_json = op.join(studydir, 'participants.json')
247+
if not op.lexists(participants_json):
248+
save_json(participants_json,
249+
OrderedDict([
250+
("participant_id", OrderedDict([
251+
("Description", "Participant identifier")])),
252+
("age", OrderedDict([
253+
("Description", "Age in years (TODO - verify) as in the initial"
254+
" session, might not be correct for other sessions")])),
255+
("sex", OrderedDict([
256+
("Description", "self-rated by participant, M for male/F for "
257+
"female (TODO: verify)")])),
258+
("group", OrderedDict([
259+
("Description", "(TODO: adjust - by default everyone is in "
260+
"control group)")])),
261+
]),
262+
sort_keys=False,
263+
indent=2)
243264
# Add a new participant
244265
with open(participants_tsv, 'a') as f:
245266
f.write(
@@ -311,7 +332,8 @@ def save_scans_key(item, bids_files):
311332

312333
def add_rows_to_scans_keys_file(fn, newrows):
313334
"""
314-
Add new rows to file fn for scans key filename
335+
Add new rows to file fn for scans key filename and generate accompanying json
336+
descriptor to make BIDS validator happy.
315337
316338
Parameters
317339
----------
@@ -334,6 +356,25 @@ def add_rows_to_scans_keys_file(fn, newrows):
334356
os.unlink(fn)
335357
else:
336358
fnames2info = newrows
359+
# Populate _scans.json (an optional file to describe column names in
360+
# _scans.tsv). This auto generation will make BIDS-validator happy.
361+
scans_json = '.'.join(fn.split('.')[:-1] + ['json'])
362+
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,
377+
indent=2)
337378

338379
header = ['filename', 'acq_time', 'operator', 'randstr']
339380
# prepare all the data rows
@@ -370,7 +411,7 @@ def get_formatted_scans_key_row(dcm_fn):
370411
time = dcm_data.ContentTime.split('.')[0]
371412
td = time + date
372413
acq_time = datetime.strptime(td, '%H%M%S%Y%m%d').isoformat()
373-
except AttributeError as exc:
414+
except (AttributeError, ValueError) as exc:
374415
lgr.warning("Failed to get date/time for the content: %s", str(exc))
375416
acq_time = None
376417
# add random string

heudiconv/cli/run.py

Lines changed: 32 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
import logging
1717
lgr = logging.getLogger(__name__)
1818

19-
INIT_MSG = "Running {packname} version {version}".format
19+
INIT_MSG = "Running {packname} version {version} latest {latest}".format
2020

2121

2222
def is_interactive():
@@ -112,6 +112,7 @@ def main(argv=None):
112112
random.seed(args.random_seed)
113113
import numpy
114114
numpy.random.seed(args.random_seed)
115+
# Ensure only supported bids options are passed
115116
if args.debug:
116117
lgr.setLevel(logging.DEBUG)
117118
# Should be possible but only with a single subject -- will be used to
@@ -129,7 +130,7 @@ def main(argv=None):
129130

130131
def get_parser():
131132
docstr = ("""Example:
132-
heudiconv -d rawdata/{subject} -o . -f heuristic.py -s s1 s2 s3""")
133+
heudiconv -d 'rawdata/{subject}' -o . -f heuristic.py -s s1 s2 s3""")
133134
parser = ArgumentParser(description=docstr)
134135
parser.add_argument('--version', action='version', version=__version__)
135136
group = parser.add_mutually_exclusive_group()
@@ -138,7 +139,12 @@ def get_parser():
138139
'subject id {subject} and session {session}. Tarballs '
139140
'(can be compressed) are supported in addition to '
140141
'directory. All matching tarballs for a subject are '
141-
'extracted and their content processed in a single pass')
142+
'extracted and their content processed in a single '
143+
'pass. If multiple tarballs are found, each is '
144+
'assumed to be a separate session and the --ses '
145+
'argument is ignored. Note that you might need to '
146+
'surround the value with quotes to avoid {...} being '
147+
'considered by shell')
142148
group.add_argument('--files', nargs='*',
143149
help='Files (tarballs, dicoms) or directories '
144150
'containing files to process. Cannot be provided if '
@@ -181,8 +187,16 @@ def get_parser():
181187
parser.add_argument('-ss', '--ses', dest='session', default=None,
182188
help='session for longitudinal study_sessions, default '
183189
'is none')
184-
parser.add_argument('-b', '--bids', action='store_true',
185-
help='flag for output into BIDS structure')
190+
parser.add_argument('-b', '--bids', nargs='*',
191+
metavar=('BIDSOPTION1', 'BIDSOPTION2'),
192+
choices=['notop'],
193+
dest='bids_options',
194+
help='flag for output into BIDS structure. Can also '
195+
'take bids specific options, e.g., --bids notop.'
196+
'The only currently supported options is'
197+
'"notop", which skips creation of top-level bids '
198+
'files. This is useful when running in batch mode to '
199+
'prevent possible race conditions.')
186200
parser.add_argument('--overwrite', action='store_true', default=False,
187201
help='flag to allow overwriting existing converted files')
188202
parser.add_argument('--datalad', action='store_true',
@@ -234,8 +248,16 @@ def process_args(args):
234248

235249
outdir = op.abspath(args.outdir)
236250

251+
import etelemetry
252+
try:
253+
latest = etelemetry.get_project("nipy/heudiconv")
254+
except Exception as e:
255+
lgr.warning("Could not check for version updates: ", e)
256+
latest = {"version": 'Unknown'}
257+
237258
lgr.info(INIT_MSG(packname=__packagename__,
238-
version=__version__))
259+
version=__version__,
260+
latest=latest["version"]))
239261

240262
if args.command:
241263
process_extra_commands(outdir, args)
@@ -302,7 +324,8 @@ def process_args(args):
302324
from ..external.dlad import prepare_datalad
303325
dlad_sid = sid if not anon_sid else anon_sid
304326
dl_msg = prepare_datalad(anon_study_outdir, anon_outdir, dlad_sid,
305-
session, seqinfo, dicoms, args.bids)
327+
session, seqinfo, dicoms,
328+
args.bids_options)
306329

307330
lgr.info("PROCESSING STARTS: {0}".format(
308331
str(dict(subject=sid, outdir=study_outdir, session=session))))
@@ -316,7 +339,7 @@ def process_args(args):
316339
anon_outdir=anon_study_outdir,
317340
with_prov=args.with_prov,
318341
ses=session,
319-
bids=args.bids,
342+
bids_options=args.bids_options,
320343
seqinfo=seqinfo,
321344
min_meta=args.minmeta,
322345
overwrite=args.overwrite,
@@ -333,7 +356,7 @@ def process_args(args):
333356
# also in batch mode might fail since we have no locking ATM
334357
# and theoretically no need actually to save entire study
335358
# we just need that
336-
add_to_datalad(outdir, study_outdir, msg, args.bids)
359+
add_to_datalad(outdir, study_outdir, msg, args.bids_options)
337360

338361
# if args.bids:
339362
# # Let's populate BIDS templates for folks to take care about

0 commit comments

Comments
 (0)