Skip to content

Commit bb5ffdf

Browse files
committed
Merge branch 'master' into fix/updater-lists
2 parents 96de779 + 3f9a504 commit bb5ffdf

34 files changed

+765
-297
lines changed

.travis.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
# vim ft=yaml
22
language: python
3+
dist: bionic
34
python:
4-
- 3.5
55
- 3.6
66
- 3.7
77
- 3.8
8+
- 3.9
89

910
cache:
1011
- apt

CHANGELOG.md

Lines changed: 85 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,78 @@ All notable changes to this project will be documented (for humans) in this file
44
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
55
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
66

7-
## [0.8.1] - Date
7+
## [0.10.0] - 2021-09-16
88

9-
TODO Summary
9+
Various improvements and compatibility/support (dcm2niix, datalad) changes.
1010

1111
### Added
12+
13+
- Add "AcquisitionTime" to the seqinfo ([#487][])
14+
- Add support for saving the Phoenix Report in the sourcedata folder ([#489][])
15+
1216
### Changed
13-
### Deprecated
17+
18+
- Python 3.5 EOLed, supported (tested) versions now: 3.6 - 3.9
19+
- In reprorin heuristic, allow for having multiple accessions since now there is
20+
`-g all` groupping ([#508][])
21+
- For BIDS, produce a singular `scans.json` at the top level, and not one per
22+
sub/ses (generates too many identical files) ([#507][])
23+
24+
1425
### Fixed
26+
27+
- Compatibility with DataLad 0.15.0. Minimal version is 0.13.0 now.
28+
- Try to open top level BIDS .json files a number of times for adjustment,
29+
so in the case of competition across parallel processes, they just end up
30+
with the last one "winning over" ([#523][])
31+
- Don't fail if etelemetry.get_project returns None ([#501][])
32+
- Consistently use `n/a` for age/sex, also handle ?M for months ([#500][])
33+
- To avoid crashing on unrelated derivatives files etc, make `find_files` to
34+
take list of topdirs (excluding `derivatives/` etc),
35+
and look for _bold only under sub-* directories ([#496][])
36+
- Ensure bvec/bval files are only created for dwi output ([#491][])
37+
1538
### Removed
16-
### Security
39+
40+
- In reproin heuristic, old hardcoded sequence renamings and filters ([#508][])
41+
42+
43+
## [0.9.0] - 2020-12-23
44+
45+
Various improvements and compatibility/support (dcm2niix, datalad,
46+
duecredit) changes. Major change is placement of output files to the
47+
target output directory during conversion.
48+
49+
### Added
50+
51+
- #454 zenodo referencing in README.rst and support for ducredit for
52+
heudiconv and reproin heuristic
53+
- #445 more tutorial references in README.md
54+
55+
### Changed
56+
57+
- [#485][] placed files during conversion right away into the target
58+
directory (with a `_heudiconv???` suffix, renamed into ultimate target
59+
name later on), which avoids hitting file size limits of /tmp ([#481][]) and
60+
helped to avoid a regression in dcm2nixx 1.0.20201102
61+
- [#477][] replaced `rec-<magnitude|phase>` with `part-<mag|phase>` now
62+
hat BIDSsupports the part entity
63+
- [#473][] made default for CogAtlasID to be a TODO URL
64+
- [#459][] made AcquisitionTime used for acq_time scans file field
65+
- [#451][] retained sub-second resolution in scans files
66+
- [#442][] refactored code so there is now heudiconv.main.workflow for
67+
more convenient use as a Python module
68+
69+
### Fixed
70+
71+
- minimal version of nipype set to 1.2.3 to guarantee correct handling
72+
of DWI files ([#480][])
73+
- `heudiconvDCM*` temporary directories are removed now ([#462][])
74+
- compatibility with DataLad 0.13 ([#464][])
75+
76+
### Removed
77+
78+
- #443 pathlib as a dependency (we are Python3 only now)
1779

1880

1981
## [0.8.0] - 2020-04-15
@@ -323,6 +385,12 @@ TODO Summary
323385
[#366]: https://github.com/nipy/heudiconv/issues/366
324386
[#368]: https://github.com/nipy/heudiconv/issues/368
325387
[#373]: https://github.com/nipy/heudiconv/issues/373
388+
[#485]: https://github.com/nipy/heudiconv/issues/485
389+
[#442]: https://github.com/nipy/heudiconv/issues/442
390+
[#451]: https://github.com/nipy/heudiconv/issues/451
391+
[#459]: https://github.com/nipy/heudiconv/issues/459
392+
[#473]: https://github.com/nipy/heudiconv/issues/473
393+
[#477]: https://github.com/nipy/heudiconv/issues/477
326394
[#293]: https://github.com/nipy/heudiconv/issues/293
327395
[#304]: https://github.com/nipy/heudiconv/issues/304
328396
[#306]: https://github.com/nipy/heudiconv/issues/306
@@ -360,11 +428,16 @@ TODO Summary
360428
[#434]: https://github.com/nipy/heudiconv/issues/434
361429
[#436]: https://github.com/nipy/heudiconv/issues/436
362430
[#437]: https://github.com/nipy/heudiconv/issues/437
363-
[#425]: https://github.com/nipy/heudiconv/issues/425
364-
[#420]: https://github.com/nipy/heudiconv/issues/420
365-
[#425]: https://github.com/nipy/heudiconv/issues/425
366-
[#430]: https://github.com/nipy/heudiconv/issues/430
367-
[#432]: https://github.com/nipy/heudiconv/issues/432
368-
[#434]: https://github.com/nipy/heudiconv/issues/434
369-
[#436]: https://github.com/nipy/heudiconv/issues/436
370-
[#437]: https://github.com/nipy/heudiconv/issues/437
431+
[#462]: https://github.com/nipy/heudiconv/issues/462
432+
[#464]: https://github.com/nipy/heudiconv/issues/464
433+
[#480]: https://github.com/nipy/heudiconv/issues/480
434+
[#481]: https://github.com/nipy/heudiconv/issues/481
435+
[#487]: https://github.com/nipy/heudiconv/issues/487
436+
[#489]: https://github.com/nipy/heudiconv/issues/489
437+
[#491]: https://github.com/nipy/heudiconv/issues/491
438+
[#496]: https://github.com/nipy/heudiconv/issues/496
439+
[#500]: https://github.com/nipy/heudiconv/issues/500
440+
[#501]: https://github.com/nipy/heudiconv/issues/501
441+
[#507]: https://github.com/nipy/heudiconv/issues/507
442+
[#508]: https://github.com/nipy/heudiconv/issues/508
443+
[#523]: https://github.com/nipy/heudiconv/issues/523

Makefile

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,18 @@
1+
PYTHON ?= python3
2+
13
all:
24
echo 'nothing by default'
35

46
prep_release:
57
# take previous one, and replace with the next one
68
utils/prep_release
9+
10+
release-pypi: prep_release
11+
# avoid upload of stale builds
12+
test ! -e dist
13+
# make sure all is still clean/committed
14+
! bash -c 'git diff | grep -q .'
15+
$(PYTHON) setup.py sdist
16+
$(PYTHON) setup.py bdist_wheel
17+
twine upload dist/*
18+

README.rst

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,10 @@
2020
:target: http://heudiconv.readthedocs.io/en/latest/?badge=latest
2121
:alt: Readthedocs
2222

23+
.. image:: https://zenodo.org/badge/DOI/10.5281/zenodo.1012598.svg
24+
:target: https://doi.org/10.5281/zenodo.1012598
25+
:alt: Zenodo (latest)
26+
2327
About
2428
-----
2529

@@ -33,3 +37,10 @@ into structured directory layouts.
3337
- it can track the provenance of the conversion from DICOM to NIfTI in W3C PROV format
3438
- it provides assistance in converting to `BIDS <http://bids.neuroimaging.io/>`_.
3539
- it integrates with `DataLad <https://www.datalad.org/>`_ to place converted and original data under git/git-annex version control, while automatically annotating files with sensitive information (e.g., non-defaced anatomicals, etc)
40+
41+
How to cite
42+
-----------
43+
44+
Please use `Zenodo record <https://doi.org/10.5281/zenodo.1012598>`_ for
45+
your specific version of HeuDiConv. We also support gathering
46+
all relevant citations via `DueCredit <http://duecredit.org>`_.

docs/conf.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
# The short X.Y version
2727
version = ''
2828
# The full version, including alpha/beta/rc tags
29-
release = '0.8.0'
29+
release = '0.10.0'
3030

3131

3232
# -- General configuration ---------------------------------------------------

docs/installation.rst

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ If `Docker <https://docs.docker.com/install/>`_ is available on your system, you
2626
can visit `our page on Docker Hub <https://hub.docker.com/r/nipy/heudiconv/tags>`_
2727
to view available releases. To pull the latest release, run::
2828

29-
$ docker pull nipy/heudiconv:0.8.0
29+
$ docker pull nipy/heudiconv:0.10.0
3030

3131

3232
Singularity
@@ -35,4 +35,4 @@ If `Singularity <https://www.sylabs.io/singularity/>`_ is available on your syst
3535
you can use it to pull and convert our Docker images! For example, to pull and
3636
build the latest release, you can run::
3737

38-
$ singularity pull docker://nipy/heudiconv:0.8.0
38+
$ singularity pull docker://nipy/heudiconv:0.10.0

docs/usage.rst

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ The second script processes a DICOM directory with ``heudiconv`` using the built
8282
DCMDIR=${DCMDIRS[${SLURM_ARRAY_TASK_ID}]}
8383
echo Submitted directory: ${DCMDIR}
8484
85-
IMG="/singularity-images/heudiconv-0.8.0-dev.sif"
85+
IMG="/singularity-images/heudiconv-0.10.0-dev.sif"
8686
CMD="singularity run -B ${DCMDIR}:/dicoms:ro -B ${OUTDIR}:/output -e ${IMG} --files /dicoms/ -o /output -f reproin -c dcm2niix -b notop --minmeta -l ."
8787
8888
printf "Command:\n${CMD}\n"
@@ -97,7 +97,7 @@ This script creates the top-level bids files (e.g.,
9797
set -eu
9898

9999
OUTDIR=${1}
100-
IMG="/singularity-images/heudiconv-0.8.0-dev.sif"
100+
IMG="/singularity-images/heudiconv-0.10.0-dev.sif"
101101
CMD="singularity run -B ${OUTDIR}:/output -e ${IMG} --files /output -f reproin --command populate-templates"
102102

103103
printf "Command:\n${CMD}\n"

heudiconv/bids.py

Lines changed: 58 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,12 @@
1818
load_json,
1919
save_json,
2020
create_file_if_missing,
21-
json_dumps_pretty,
21+
json_dumps,
2222
set_readonly,
2323
is_readonly,
2424
get_datetime,
2525
)
26+
from . import __version__
2627

2728
lgr = logging.getLogger(__name__)
2829

@@ -40,10 +41,47 @@
4041
("Description", "md5 hash of UIDs")])),
4142
])
4243

44+
#: JSON Key where we will embed our version in the newly produced .json files
45+
HEUDICONV_VERSION_JSON_KEY = 'HeudiconvVersion'
46+
47+
4348
class BIDSError(Exception):
4449
pass
4550

4651

52+
BIDS_VERSION = "1.4.1"
53+
54+
55+
def maybe_na(val):
56+
"""Return 'n/a' if non-None value represented as str is not empty
57+
58+
Primarily for the consistent use of lower case 'n/a' so 'N/A' and 'NA'
59+
are also treated as 'n/a'
60+
"""
61+
if val is not None:
62+
val = str(val)
63+
val = val.strip()
64+
return 'n/a' if (not val or val in ('N/A', 'NA')) else val
65+
66+
67+
def treat_age(age):
68+
"""Age might encounter 'Y' suffix or be a float"""
69+
age = str(age)
70+
if age.endswith('M'):
71+
age = age.rstrip('M')
72+
age = float(age) / 12
73+
age = ('%.2f' if age != int(age) else '%d') % age
74+
else:
75+
age = age.rstrip('Y')
76+
if age:
77+
# strip all leading 0s but allow to scan a newborn (age 0Y)
78+
age = '0' if not age.lstrip('0') else age.lstrip('0')
79+
if age.startswith('.'):
80+
# we had float point value, let's prepend 0
81+
age = '0' + age
82+
return age
83+
84+
4785
def populate_bids_templates(path, defaults={}):
4886
"""Premake BIDS text files with templates"""
4987

@@ -53,7 +91,7 @@ def populate_bids_templates(path, defaults={}):
5391
save_json(descriptor,
5492
OrderedDict([
5593
('Name', "TODO: name of the dataset"),
56-
('BIDSVersion', "1.0.1"),
94+
('BIDSVersion', BIDS_VERSION),
5795
('License', defaults.get('License',
5896
"TODO: choose a license, e.g. PDDL "
5997
"(http://opendatacommons.org/licenses/pddl/)")),
@@ -87,6 +125,9 @@ def populate_bids_templates(path, defaults={}):
87125
create_file_if_missing(op.join(path, 'README'),
88126
"TODO: Provide description for the dataset -- basic details about the "
89127
"study, possibly pointing to pre-registration (if public or embargoed)")
128+
create_file_if_missing(op.join(path, 'scans.json'),
129+
json_dumps(SCANS_FILE_FIELDS, sort_keys=False)
130+
)
90131

91132
populate_aggregated_jsons(path)
92133

@@ -111,7 +152,8 @@ def populate_aggregated_jsons(path):
111152
# way too many -- let's just collect all which are the same!
112153
# FIELDS_TO_TRACK = {'RepetitionTime', 'FlipAngle', 'EchoTime',
113154
# 'Manufacturer', 'SliceTiming', ''}
114-
for fpath in find_files('.*_task-.*\_bold\.json', topdir=path,
155+
for fpath in find_files('.*_task-.*\_bold\.json',
156+
topdir=glob(op.join(path, 'sub-*')),
115157
exclude_vcs=True,
116158
exclude="/\.(datalad|heudiconv)/"):
117159
#
@@ -120,7 +162,7 @@ def populate_aggregated_jsons(path):
120162
# TODO: if we are to fix it, then old ones (without _acq) should be
121163
# removed first
122164
task = re.sub('.*_(task-[^_\.]*(_acq-[^_\.]*)?)_.*', r'\1', fpath)
123-
json_ = load_json(fpath)
165+
json_ = load_json(fpath, retry=100)
124166
if task not in tasks:
125167
tasks[task] = json_
126168
else:
@@ -172,10 +214,10 @@ def populate_aggregated_jsons(path):
172214
placeholders = {
173215
"TaskName": ("TODO: full task name for %s" %
174216
task_acq.split('_')[0].split('-')[1]),
175-
"CogAtlasID": "TODO",
217+
"CogAtlasID": "http://www.cognitiveatlas.org/task/id/TODO",
176218
}
177219
if op.lexists(task_file):
178-
j = load_json(task_file)
220+
j = load_json(task_file, retry=100)
179221
# Retain possibly modified placeholder fields
180222
for f in placeholders:
181223
if f in j:
@@ -207,6 +249,10 @@ def tuneup_bids_json_files(json_files):
207249
# Let's hope no word 'Date' comes within a study name or smth like
208250
# that
209251
raise ValueError("There must be no dates in .json sidecar")
252+
# Those files should not have our version field already - should have been
253+
# freshly produced
254+
assert HEUDICONV_VERSION_JSON_KEY not in json_
255+
json_[HEUDICONV_VERSION_JSON_KEY] = str(__version__)
210256
save_json(jsonfile, json_)
211257

212258
# Load the beast
@@ -274,12 +320,13 @@ def add_participant_record(studydir, subject, age, sex):
274320
"control group)")])),
275321
]),
276322
sort_keys=False)
323+
277324
# Add a new participant
278325
with open(participants_tsv, 'a') as f:
279326
f.write(
280327
'\t'.join(map(str, [participant_id,
281-
age.lstrip('0').rstrip('Y') if age else 'N/A',
282-
sex,
328+
maybe_na(treat_age(age)),
329+
maybe_na(sex),
283330
'control'])) + '\n')
284331

285332

@@ -369,11 +416,6 @@ def add_rows_to_scans_keys_file(fn, newrows):
369416
os.unlink(fn)
370417
else:
371418
fnames2info = newrows
372-
# Populate _scans.json (an optional file to describe column names in
373-
# _scans.tsv). This auto generation will make BIDS-validator happy.
374-
scans_json = '.'.join(fn.split('.')[:-1] + ['json'])
375-
if not op.lexists(scans_json):
376-
save_json(scans_json, SCANS_FILE_FIELDS, sort_keys=False)
377419

378420
header = SCANS_FILE_FIELDS
379421
# prepare all the data rows
@@ -404,10 +446,10 @@ def get_formatted_scans_key_row(dcm_fn):
404446
"""
405447
dcm_data = dcm.read_file(dcm_fn, stop_before_pixels=True, force=True)
406448
# we need to store filenames and acquisition times
407-
# parse date and time and get it into isoformat
449+
# parse date and time of start of run acquisition and get it into isoformat
408450
try:
409-
date = dcm_data.ContentDate
410-
time = dcm_data.ContentTime
451+
date = dcm_data.AcquisitionDate
452+
time = dcm_data.AcquisitionTime
411453
acq_time = get_datetime(date, time)
412454
except (AttributeError, ValueError) as exc:
413455
lgr.warning("Failed to get date/time for the content: %s", str(exc))

heudiconv/cli/run.py

100644100755
File mode changed.

0 commit comments

Comments
 (0)