Skip to content

Commit 90e392e

Browse files
mgxdmadisoth
andauthored
FIX: --cifti-output missing required space (#212)
* CI: Increase machine resources, anchor job skipping * FIX: Include full specs when parsing for MNIInfant key * FIX: Re-add non-native MNIInfant to output spaces if generating CIFTIs * TST: Recify configuration test * FIX: Backport pre-crownmask fMRIPlot * CI: Remove cifti output Because we are working with very downsampled data, functional segmentation is very unreliable * STY: Flake8 * doc: remove scipy restriction * TST: Fix failing tests * Update nibabies/utils/viz.py Co-authored-by: Thomas Madison <[email protected]> * ENH: Add GIFTI masking interface * FIX: Ensure grayord surfaces are properly masked * FIX: Return runtime duh * ENH: Use CreateDenseTimeSeries rois for surface masking * PIN: Avoid nipype 1.8.0 Co-authored-by: Thomas Madison <[email protected]>
1 parent 60441a1 commit 90e392e

File tree

11 files changed

+1350
-63
lines changed

11 files changed

+1350
-63
lines changed

.circleci/config.yml

Lines changed: 41 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ _machine_defaults: &machine_defaults
66
image: ubuntu-2004:current
77
docker_layer_caching: true
88
working_directory: /tmp/src/nibabies
9+
resource_class: large
910

1011
_python_defaults: &python_defaults
1112
docker:
@@ -39,6 +40,28 @@ _pull_from_registry: &pull_from_registry
3940
docker pull localhost:5000/nibabies
4041
docker tag localhost:5000/nibabies nipreps/nibabies:latest
4142
43+
_check_skip_job: &check_skip_job
44+
name: Check commit message and determine if job should be skipped
45+
command: |
46+
set -x +e
47+
COMMIT_MSG="$(git log --format='format:%s' -n 1 $CIRCLE_SHA1)"
48+
DOCBUILD="$(echo ${COMMIT_MSG} | grep -i -E '^docs?(\(\w+\))?:')"
49+
SKIP_BCP="$(echo ${COMMIT_MSG} | grep -i -E '\[skip[ _]?bcp\]' )"
50+
# no skipping if tagged build
51+
if [[ -n "$CIRCLETAG" ]]; then
52+
exit 0
53+
elif [[ -n "$DOCSBUILD" ]]; then # always try to skip docs builds
54+
echo "Only docs build"
55+
circleci step halt
56+
elif [ -n "$CHECK_PYTEST" -a -n "$SKIP_PYTEST" ]; then
57+
echo "Skipping pytest"
58+
circleci step halt
59+
elif [ -n "$CHECK_BCP" -a -n "$SKIP_BCP" ]; then
60+
echo "Skipping BCP"
61+
circleci step halt
62+
fi
63+
echo "No skip"
64+
4265
version: 2.1
4366
orbs:
4467
docker: circleci/[email protected]
@@ -49,13 +72,7 @@ jobs:
4972
<<: *machine_defaults
5073
steps:
5174
- checkout
52-
- run:
53-
name: Check whether build should be skipped
54-
command: |
55-
if [[ "$( git log --format='format:%s' -n 1 $CIRCLE_SHA1 | grep -i -E '^docs?(\(\w+\))?:' )" != "" ]]; then
56-
echo "Only docs build"
57-
circleci step halt
58-
fi
75+
- run: *check_skip_job
5976
- restore_cache:
6077
keys:
6178
- build-v4-{{ .Branch }}-{{ .Revision }}
@@ -154,6 +171,7 @@ jobs:
154171
- /tmp/docker
155172
- /tmp/images
156173

174+
157175
get_data:
158176
docker:
159177
- image: continuumio/miniconda3:4.10.3-alpine
@@ -214,19 +232,11 @@ jobs:
214232

215233
test_pytest:
216234
<<: *machine_defaults
235+
environment:
236+
CHECK_PYTEST: true
217237
steps:
218238
- checkout
219-
- run:
220-
name: Check whether build should be skipped
221-
command: |
222-
if [[ "$( git log --format='format:%s' -n 1 $CIRCLE_SHA1 | grep -i -E '^docs?(\(\w+\))?:' )" != "" ]]; then
223-
echo "Only docs build"
224-
circleci step halt
225-
fi
226-
if [[ "$( git log --format=oneline -n 1 $CIRCLE_SHA1 | grep -i -E '\[skip[ _]?tests\]' )" != "" ]]; then
227-
echo "Skipping pytest job"
228-
circleci step halt
229-
fi
239+
- run: *check_skip_job
230240
- attach_workspace:
231241
at: /tmp
232242
- restore_cache:
@@ -279,28 +289,16 @@ jobs:
279289
- store_artifacts:
280290
path: /tmp/data/reports
281291

292+
282293
test_bcp:
294+
<<: *machine_defaults
283295
environment:
284296
- FS_LICENSE: /tmp/fslicense/license.txt
285297
- DATASET: bcp
286-
<<: *machine_defaults
298+
- CHECK_BCP: true
287299
steps:
288300
- checkout
289-
- run:
290-
name: Check whether build should be skipped
291-
command: |
292-
if [[ "$( git log --format='format:%s' -n 1 $CIRCLE_SHA1 | grep -i -E '^docs?(\(\w+\))?:' )" != "" ]]; then
293-
echo "Only docs build"
294-
circleci step halt
295-
fi
296-
if [[ "$( git log --format=oneline -n 1 $CIRCLE_SHA1 | grep -i -E '\[skip[ _]?bcp\]' )" != "" ]]; then
297-
echo "Skipping bcp build"
298-
circleci step halt
299-
fi
300-
if [[ "$( git log --format=oneline -n 1 $CIRCLE_SHA1 | grep -i -E '\[no[ _-]?fasttrack\]' )" != "" ]]; then
301-
touch /tmp/.nofasttrack
302-
echo "Anatomical fasttrack reusing sMRIPrep's derivatives will not be used."
303-
fi
301+
- run: *check_skip_job
304302
- attach_workspace:
305303
at: /tmp
306304
- restore_cache:
@@ -345,8 +343,8 @@ jobs:
345343
--fs-subjects-dir /tmp/data/${DATASET}/derivatives/infant-freesurfer \
346344
--skull-strip-template UNCInfant:cohort-1 \
347345
--output-spaces MNIInfant:cohort-1 func \
348-
--sloppy --write-graph --mem_mb 8192 \
349-
--nthreads 2 -vv --age-months 2 --sloppy \
346+
--sloppy --write-graph --mem-mb 14000 \
347+
--nthreads 4 -vv --age-months 2 --sloppy \
350348
--derivatives /tmp/data/${DATASET}/derivatives/precomputed \
351349
--output-layout bids --anat-only
352350
- run:
@@ -371,8 +369,8 @@ jobs:
371369
--fs-subjects-dir /tmp/data/${DATASET}/derivatives/infant-freesurfer \
372370
--skull-strip-template UNCInfant:cohort-1 \
373371
--output-spaces MNIInfant:cohort-1 func \
374-
--sloppy --write-graph --mem_mb 8192 \
375-
--nthreads 2 -vv --age-months 2 --sloppy \
372+
--sloppy --write-graph --mem-mb 14000 \
373+
--nthreads 4 -vv --age-months 2 --sloppy \
376374
--derivatives /tmp/data/${DATASET}/derivatives/precomputed \
377375
--output-layout bids
378376
- run:
@@ -388,6 +386,7 @@ jobs:
388386
- store_artifacts:
389387
path: /tmp/bcp/derivatives
390388

389+
391390
deploy_docker_patches:
392391
<<: *machine_defaults
393392
steps:
@@ -423,6 +422,7 @@ jobs:
423422
docker tag nipreps/nibabies nipreps/nibabies:${CIRCLE_BRANCH#docker/}
424423
docker push nipreps/nibabies:${CIRCLE_BRANCH#docker/}
425424
425+
426426
deploy_docker:
427427
<<: *machine_defaults
428428
steps:
@@ -459,6 +459,7 @@ jobs:
459459
fi
460460
fi
461461
462+
462463
test_deploy_pypi:
463464
<<: *python_defaults
464465
steps:
@@ -528,6 +529,7 @@ jobs:
528529
- store_artifacts:
529530
path: /tmp/src/nibabies/wrapper/dist
530531

532+
531533
deploy_pypi:
532534
<<: *python_defaults
533535
steps:
@@ -564,6 +566,7 @@ jobs:
564566
export TWINE_PASSWORD=$PYPI_WRAPPER_TOKEN
565567
python -m twine upload wrapper/dist/nibabies*
566568
569+
567570
deployable:
568571
docker:
569572
- image: busybox:latest

docs/requirements.txt

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,3 @@ myst_nb
33
sphinx-argparse
44
# Relative to repository root
55
./wrapper/
6-
# avoid downloading new matplotlib
7-
scipy ~= 1.7.3

nibabies/config.py

Lines changed: 6 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -722,18 +722,6 @@ def init_spaces(checkpoint=True):
722722
if checkpoint and not spaces.is_cached():
723723
spaces.checkpoint()
724724

725-
# NiBabies' default volumetric space is MNIInfant
726-
# However, if age is not provided, do not add a default template as we cannot
727-
# make an assumption about cohort.
728-
if (
729-
not any(s.space == 'MNIInfant' for s in spaces.references)
730-
and workflow.age_months is not None
731-
):
732-
from .utils.misc import cohort_by_months
733-
734-
cohort = cohort_by_months("MNIInfant", workflow.age_months)
735-
spaces.add(Reference("MNIInfant", {"cohort": cohort}))
736-
737725
# Ensure user-defined spatial references for outputs are correctly parsed.
738726
# Certain options require normalization to a space not explicitly defined by users.
739727
# These spaces will not be included in the final outputs.
@@ -746,6 +734,12 @@ def init_spaces(checkpoint=True):
746734
vol_res = "2" if workflow.cifti_output == "91k" else "1"
747735
spaces.add(Reference("fsaverage", {"den": "164k"}))
748736
spaces.add(Reference("MNI152NLin6Asym", {"res": vol_res}))
737+
# Ensure a non-native version of MNIInfant is added as a target
738+
if workflow.age_months is not None:
739+
from .utils.misc import cohort_by_months
740+
741+
cohort = cohort_by_months("MNIInfant", workflow.age_months)
742+
spaces.add(Reference("MNIInfant", {"cohort": cohort}))
749743

750744
# Make the SpatialReferences object available
751745
workflow.spaces = spaces

nibabies/conftest.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,3 +34,4 @@ def data_dir():
3434
def set_namespace(doctest_namespace, data_dir):
3535
doctest_namespace["data_dir"] = data_dir
3636
doctest_namespace["test_data"] = Path(resource_filename("nibabies", "tests/data"))
37+
doctest_namespace["Path"] = Path

nibabies/interfaces/confounds.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -367,7 +367,9 @@ class FMRISummary(SimpleInterface):
367367
output_spec = FMRISummaryOutputSpec
368368

369369
def _run_interface(self, runtime):
370-
from niworkflows.viz.plots import fMRIPlot
370+
# Backwards-compatible fMRIPlot
371+
# TODO: Replace with niworkflows next minor version
372+
from ..utils.viz import fMRIPlot
371373

372374
self._results["out_file"] = fname_presuffix(
373375
self.inputs.in_func, suffix="_fmriplot.svg", use_ext=False, newpath=runtime.cwd

nibabies/interfaces/gifti.py

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
from pathlib import Path
2+
3+
import nibabel as nb
4+
import nibabel.gifti as ngi
5+
import numpy as np
6+
from nipype.interfaces.base import (
7+
BaseInterfaceInputSpec,
8+
File,
9+
SimpleInterface,
10+
TraitedSpec,
11+
traits,
12+
)
13+
14+
from .. import __version__
15+
16+
17+
class _MaskGiftiInputSpec(BaseInterfaceInputSpec):
18+
in_file = File(exists=True, mandatory=True, desc="Input GIFTI (n-darrays)")
19+
mask_file = File(exists=True, mandatory=True, desc="Input mask (single binary darray)")
20+
threshold = traits.Float(
21+
desc="If mask is probabilistic, inclusion limit",
22+
)
23+
metadata = traits.Dict(
24+
desc="Metadata to insert into GIFTI",
25+
)
26+
27+
28+
class _MaskGiftiOutputSpec(TraitedSpec):
29+
out_file = File(desc="Masked file")
30+
31+
32+
class MaskGifti(SimpleInterface):
33+
"""Mask file across GIFTI darrays"""
34+
35+
input_spec = _MaskGiftiInputSpec
36+
output_spec = _MaskGiftiOutputSpec
37+
38+
def _run_interface(self, runtime):
39+
self._results["out_file"] = _mask_gifti(
40+
self.inputs.in_file,
41+
self.inputs.mask_file,
42+
threshold=self.inputs.threshold or None,
43+
metadata=self.inputs.metadata,
44+
newpath=runtime.cwd,
45+
)
46+
return runtime
47+
48+
49+
def _mask_gifti(in_file, mask_file, *, threshold=None, metadata=None, newpath=None):
50+
"""
51+
Mask and create a GIFTI image.
52+
"""
53+
metadata = metadata or {}
54+
55+
img = nb.load(in_file)
56+
mask = nb.load(mask_file).agg_data()
57+
58+
indices = np.nonzero(mask)[0]
59+
if threshold is not None:
60+
indices = np.where(mask > threshold)[0]
61+
62+
data = img.agg_data()
63+
if isinstance(data, tuple):
64+
try:
65+
data = np.vstack(data)
66+
except Exception:
67+
raise NotImplementedError(f"Tricky GIFTI: {in_file} not supported.")
68+
else:
69+
data = data.T
70+
masked = data[:, indices]
71+
72+
# rather than creating new GiftiDataArrays, just modify the data directly
73+
# and preserve the existing attributes
74+
for i, darr in enumerate(img.darrays):
75+
darr.data = masked[i]
76+
darr.dims = list(masked[i].shape)
77+
78+
# Finalize by adding additional metadata to file
79+
metad = {
80+
**{"CreatedBy": f"MaskGifti (NiBabies-{__version__})"},
81+
**metadata,
82+
}
83+
if int(nb.__version__[0]) >= 4: # API will change in 4.0.0
84+
existing_meta = img.meta or {}
85+
img.meta = ngi.GiftiMetaData({**metad, **existing_meta})
86+
else:
87+
meta = img.meta.data or []
88+
for k, v in metad.items():
89+
meta.append(ngi.GiftiNVPairs(k, v))
90+
img.meta.data = meta
91+
92+
if newpath is None:
93+
newpath = Path()
94+
out_file = str((Path(newpath) / f"masked_{Path(in_file).name}").absolute())
95+
nb.save(img, out_file)
96+
return out_file

nibabies/tests/test_config.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -102,19 +102,23 @@ def test_config_spaces():
102102
if s.standard and s.dim == 3
103103
] == []
104104

105-
# but adding age will populate output spaces with a default
106105
config.execution.output_spaces = None
106+
config.workflow.cifti_output = "91k"
107107
config.workflow.use_aroma = False
108108
config.workflow.age_months = 1
109109
config.init_spaces()
110110
spaces = config.workflow.spaces
111111

112-
assert [str(s) for s in spaces.get_standard(full_spec=True)] == []
112+
assert [str(s) for s in spaces.get_standard(full_spec=True)] == [
113+
'fsaverage:den-164k',
114+
'MNI152NLin6Asym:res-2',
115+
]
116+
113117
assert [
114118
format_reference((s.fullname, s.spec))
115119
for s in spaces.references
116120
if s.standard and s.dim == 3
117-
] == ['MNIInfant_cohort-1']
121+
] == ['MNI152NLin6Asym_res-2', 'MNIInfant_cohort-1']
118122
_reset_config()
119123

120124

0 commit comments

Comments
 (0)