Skip to content

Commit dc500a6

Browse files
authored
Merge pull request #2194 from FCP-INDI/many_pipes
🐛 Bugfixes for many pipelines project
2 parents 6a7d31a + c203faf commit dc500a6

25 files changed

+1327
-228
lines changed

.circleci/main.yml

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -45,10 +45,6 @@ commands:
4545
- run:
4646
name: "Configuring git user"
4747
command: |
48-
sudo apt-key adv --recv-keys --keyserver keyserver.ubuntu.com 78BD65473CB3BD13
49-
curl -L https://packagecloud.io/circleci/trusty/gpgkey | sudo apt-key add -
50-
sudo apt-get update
51-
sudo apt-get install git -y
5248
git config --global user.email "[email protected]"
5349
git config --global user.name "Theodore (machine user) @ CircleCI"
5450
create-docker-test-container:
@@ -64,11 +60,6 @@ commands:
6460
mkdir -p ~/project/test-results
6561
docker pull ${DOCKER_TAG}
6662
docker run -v /etc/passwd:/etc/passwd --user=$(id -u):c-pac -dit -P -e COVERAGE_FILE=<< parameters.coverage-file >> -v /home/circleci/project/test-results:/code/test-results -v /home/circleci:/home/circleci -v /home/circleci/project/CPAC/resources/configs/test_configs:/test_configs -v $PWD:/code -v $PWD/dev/circleci_data:$PWD/dev/circleci_data --workdir=/home/circleci/project --entrypoint=/bin/bash --name docker_test ${DOCKER_TAG}
67-
get-sample-bids-data:
68-
steps:
69-
- run:
70-
name: Getting Sample BIDS Data
71-
command: git clone https://github.com/bids-standard/bids-examples.git
7263
get-singularity:
7364
parameters:
7465
version:
@@ -231,7 +222,6 @@ jobs:
231222
- set-up-variant:
232223
variant: "<< parameters.variant >>"
233224
- set-python-version
234-
- get-sample-bids-data
235225
- run-pytest-docker
236226
- store_test_results:
237227
path: test-results

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2424
- Required positional parameter "wf" in input and output of `ingress_pipeconfig_paths` function, where a node to reorient templates is added to the `wf`.
2525
- Required positional parameter "orientation" to `resolve_resolution`.
2626
- Optional positional argument "cfg" to `create_lesion_preproc`.
27+
- Allow enabling `overwrite_transform` only when the registration method is `ANTS`.
28+
- `resource_inventory` utility to inventory NodeBlock function inputs and outputs.
2729
- New switch `mask_sbref` under `func_input_prep` in functional registration and set to default `on`.
2830
- New resource `desc-head_bold` as non skull-stripped bold from nodeblock `bold_masking`.
2931
- `censor_file_path` from `offending_timepoints_connector` in the `build_nuisance_regressor` node.
@@ -33,6 +35,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
3335
- Moved `pygraphviz` from requirements to `graphviz` optional dependencies group.
3436
- Automatically tag untagged `subject_id` and `unique_id` as `!!str` when loading data config files.
3537
- Made orientation configurable (was hard-coded as "RPI").
38+
- Resource-not-found errors now include information about where to source those resources.
3639
- Moved `ref_mask_res_2` and `T1w_template_res-2` fields from registration into surface under `abcd_prefreesurfer_prep`.
3740
- Moved `find_censors node` inside `create_nuisance_regression_workflow` into its own function/subworkflow as `offending_timepoints_connector`.
3841
- [FSL-AFNI subworkflow](https://github.com/FCP-INDI/C-PAC/blob/4bdd6c410ef0a9b90f53100ea005af1f7d6e76c0/CPAC/func_preproc/func_preproc.py#L1052C4-L1231C25)

CPAC/_entrypoints/run.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -454,6 +454,14 @@ def run_main():
454454
action="store_true",
455455
)
456456

457+
parser.add_argument(
458+
"--freesurfer_dir",
459+
"--freesurfer-dir",
460+
help="Specify path to pre-computed FreeSurfer outputs "
461+
"to pull into C-PAC run",
462+
default=False,
463+
)
464+
457465
# get the command line arguments
458466
args = parser.parse_args(
459467
sys.argv[1 : (sys.argv.index("--") if "--" in sys.argv else len(sys.argv))]
@@ -743,6 +751,9 @@ def run_main():
743751
c["pipeline_setup", "system_config", "num_participants_at_once"],
744752
)
745753

754+
if args.freesurfer_dir:
755+
c["pipeline_setup"]["freesurfer_dir"] = args.freesurfer_dir
756+
746757
if not args.data_config_file:
747758
WFLOGGER.info("Input directory: %s", bids_dir)
748759

@@ -783,9 +794,8 @@ def run_main():
783794
sub_list = load_cpac_data_config(
784795
args.data_config_file, args.participant_label, args.aws_input_creds
785796
)
786-
list(sub_list)
787797
sub_list = sub_list_filter_by_labels(
788-
sub_list, {"T1w": args.T1w_label, "bold": args.bold_label}
798+
list(sub_list), {"T1w": args.T1w_label, "bold": args.bold_label}
789799
)
790800

791801
# C-PAC only handles single anatomical images (for now)

CPAC/_global_fixtures.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# Copyright (C) 2025 C-PAC Developers
2+
3+
# This file is part of C-PAC.
4+
5+
# C-PAC is free software: you can redistribute it and/or modify it under
6+
# the terms of the GNU Lesser General Public License as published by the
7+
# Free Software Foundation, either version 3 of the License, or (at your
8+
# option) any later version.
9+
10+
# C-PAC is distributed in the hope that it will be useful, but WITHOUT
11+
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
12+
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
13+
# License for more details.
14+
15+
# You should have received a copy of the GNU Lesser General Public
16+
# License along with C-PAC. If not, see <https://www.gnu.org/licenses/>.
17+
"""Global fixtures for C-PAC tests."""
18+
19+
from pathlib import Path
20+
21+
from _pytest.tmpdir import TempPathFactory
22+
from git import Repo
23+
import pytest
24+
25+
26+
@pytest.fixture(scope="session")
27+
def bids_examples(tmp_path_factory: TempPathFactory) -> Path:
28+
"""Get the BIDS examples dataset."""
29+
example_dir = tmp_path_factory.mktemp("bids-examples")
30+
if not example_dir.exists() or not any(example_dir.iterdir()):
31+
Repo.clone_from(
32+
"https://github.com/bids-standard/bids-examples.git", str(example_dir)
33+
)
34+
return example_dir

CPAC/anat_preproc/anat_preproc.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# -*- coding: utf-8 -*-
2-
# Copyright (C) 2012-2023 C-PAC Developers
2+
# Copyright (C) 2012-2025 C-PAC Developers
33

44
# This file is part of C-PAC.
55

@@ -2572,7 +2572,7 @@ def brain_mask_acpc_niworkflows_ants_T2(wf, cfg, strat_pool, pipe_num, opt=None)
25722572
config=["anatomical_preproc", "brain_extraction"],
25732573
option_key="using",
25742574
option_val="UNet",
2575-
inputs=["desc-preproc_T2w", "T1w-brain-template", "T1w-template", "unet_model"],
2575+
inputs=["desc-preproc_T2w", "T1w-brain-template", "T1w-template", "unet-model"],
25762576
outputs=["space-T2w_desc-brain_mask"],
25772577
)
25782578
def brain_mask_unet_T2(wf, cfg, strat_pool, pipe_num, opt=None):
@@ -2586,7 +2586,7 @@ def brain_mask_unet_T2(wf, cfg, strat_pool, pipe_num, opt=None):
25862586
config=["anatomical_preproc", "brain_extraction"],
25872587
option_key="using",
25882588
option_val="UNet",
2589-
inputs=["desc-preproc_T2w", "T1w-brain-template", "T1w-template", "unet_model"],
2589+
inputs=["desc-preproc_T2w", "T1w-brain-template", "T1w-template", "unet-model"],
25902590
outputs=["space-T2w_desc-acpcbrain_mask"],
25912591
)
25922592
def brain_mask_acpc_unet_T2(wf, cfg, strat_pool, pipe_num, opt=None):

CPAC/conftest.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# Copyright (C) 2025 C-PAC Developers
2+
3+
# This file is part of C-PAC.
4+
5+
# C-PAC is free software: you can redistribute it and/or modify it under
6+
# the terms of the GNU Lesser General Public License as published by the
7+
# Free Software Foundation, either version 3 of the License, or (at your
8+
# option) any later version.
9+
10+
# C-PAC is distributed in the hope that it will be useful, but WITHOUT
11+
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
12+
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
13+
# License for more details.
14+
15+
# You should have received a copy of the GNU Lesser General Public
16+
# License along with C-PAC. If not, see <https://www.gnu.org/licenses/>.
17+
"""Global fixtures for C-PAC tests."""
18+
19+
from CPAC._global_fixtures import bids_examples
20+
21+
__all__ = ["bids_examples"]

CPAC/func_preproc/func_preproc.py

Lines changed: 114 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1503,6 +1503,91 @@ def bold_mask_anatomical_based(wf, cfg, strat_pool, pipe_num, opt=None):
15031503
return (wf, outputs)
15041504

15051505

1506+
def anat_brain_to_bold_res(wf_name, cfg, pipe_num):
1507+
wf = pe.Workflow(name=f"{wf_name}_{pipe_num}")
1508+
1509+
inputNode = pe.Node(
1510+
util.IdentityInterface(
1511+
fields=["T1w-template-funcreg", "space-template_desc-preproc_T1w"]
1512+
),
1513+
name="inputspec",
1514+
)
1515+
outputNode = pe.Node(
1516+
util.IdentityInterface(fields=["space-template_res-bold_desc-brain_T1w"]),
1517+
name="outputspec",
1518+
)
1519+
1520+
# applywarp --rel --interp=spline -i ${T1wImage} -r ${ResampRefIm} --premat=$FSLDIR/etc/flirtsch/ident.mat -o ${WD}/${T1wImageFile}.${FinalfMRIResolution}
1521+
anat_brain_to_func_res = pe.Node(
1522+
interface=fsl.ApplyWarp(), name=f"resample_anat_brain_in_standard_{pipe_num}"
1523+
)
1524+
1525+
anat_brain_to_func_res.inputs.interp = "spline"
1526+
anat_brain_to_func_res.inputs.premat = cfg.registration_workflows[
1527+
"anatomical_registration"
1528+
]["registration"]["FSL-FNIRT"]["identity_matrix"]
1529+
1530+
wf.connect(
1531+
inputNode, "space-template_desc-preproc_T1w", anat_brain_to_func_res, "in_file"
1532+
)
1533+
wf.connect(inputNode, "T1w-template-funcreg", anat_brain_to_func_res, "ref_file")
1534+
1535+
wf.connect(
1536+
anat_brain_to_func_res,
1537+
"out_file",
1538+
outputNode,
1539+
"space-template_res-bold_desc-brain_T1w",
1540+
)
1541+
return wf
1542+
1543+
1544+
def anat_brain_mask_to_bold_res(wf_name, cfg, pipe_num):
1545+
# Create brain masks in this space from the FreeSurfer output (changing resolution)
1546+
# applywarp --rel --interp=nn -i ${FreeSurferBrainMask}.nii.gz -r ${WD}/${T1wImageFile}.${FinalfMRIResolution} --premat=$FSLDIR/etc/flirtsch/ident.mat -o ${WD}/${FreeSurferBrainMaskFile}.${FinalfMRIResolution}.nii.gz
1547+
wf = pe.Workflow(name=f"{wf_name}_{pipe_num}")
1548+
inputNode = pe.Node(
1549+
util.IdentityInterface(
1550+
fields=["space-template_desc-brain_mask", "space-template_desc-preproc_T1w"]
1551+
),
1552+
name="inputspec",
1553+
)
1554+
outputNode = pe.Node(
1555+
util.IdentityInterface(fields=["space-template_desc-bold_mask"]),
1556+
name="outputspec",
1557+
)
1558+
1559+
anat_brain_mask_to_func_res = pe.Node(
1560+
interface=fsl.ApplyWarp(),
1561+
name=f"resample_anat_brain_mask_in_standard_{pipe_num}",
1562+
)
1563+
1564+
anat_brain_mask_to_func_res.inputs.interp = "nn"
1565+
anat_brain_mask_to_func_res.inputs.premat = cfg.registration_workflows[
1566+
"anatomical_registration"
1567+
]["registration"]["FSL-FNIRT"]["identity_matrix"]
1568+
1569+
wf.connect(
1570+
inputNode,
1571+
"space-template_desc-brain_mask",
1572+
anat_brain_mask_to_func_res,
1573+
"in_file",
1574+
)
1575+
wf.connect(
1576+
inputNode,
1577+
"space-template_desc-preproc_T1w",
1578+
anat_brain_mask_to_func_res,
1579+
"ref_file",
1580+
)
1581+
wf.connect(
1582+
anat_brain_mask_to_func_res,
1583+
"out_file",
1584+
outputNode,
1585+
"space-template_desc-bold_mask",
1586+
)
1587+
1588+
return wf
1589+
1590+
15061591
@nodeblock(
15071592
name="bold_mask_anatomical_resampled",
15081593
switch=[
@@ -1528,39 +1613,35 @@ def bold_mask_anatomical_resampled(wf, cfg, strat_pool, pipe_num, opt=None):
15281613
15291614
Adapted from `DCAN Lab's BOLD mask method from the ABCD pipeline <https://github.com/DCAN-Labs/DCAN-HCP/blob/1d90814/fMRIVolume/scripts/OneStepResampling.sh#L121-L132>`_.
15301615
"""
1531-
# applywarp --rel --interp=spline -i ${T1wImage} -r ${ResampRefIm} --premat=$FSLDIR/etc/flirtsch/ident.mat -o ${WD}/${T1wImageFile}.${FinalfMRIResolution}
1532-
anat_brain_to_func_res = pe.Node(
1533-
interface=fsl.ApplyWarp(), name=f"resample_anat_brain_in_standard_{pipe_num}"
1534-
)
1535-
1536-
anat_brain_to_func_res.inputs.interp = "spline"
1537-
anat_brain_to_func_res.inputs.premat = cfg.registration_workflows[
1538-
"anatomical_registration"
1539-
]["registration"]["FSL-FNIRT"]["identity_matrix"]
1616+
anat_brain_to_func_res = anat_brain_to_bold_res(wf, cfg, pipe_num)
15401617

15411618
node, out = strat_pool.get_data("space-template_desc-preproc_T1w")
1542-
wf.connect(node, out, anat_brain_to_func_res, "in_file")
1619+
wf.connect(
1620+
node, out, anat_brain_to_func_res, "inputspec.space-template_desc-preproc_T1w"
1621+
)
15431622

15441623
node, out = strat_pool.get_data("T1w-template-funcreg")
1545-
wf.connect(node, out, anat_brain_to_func_res, "ref_file")
1624+
wf.connect(node, out, anat_brain_to_func_res, "inputspec.T1w-template-funcreg")
15461625

15471626
# Create brain masks in this space from the FreeSurfer output (changing resolution)
15481627
# applywarp --rel --interp=nn -i ${FreeSurferBrainMask}.nii.gz -r ${WD}/${T1wImageFile}.${FinalfMRIResolution} --premat=$FSLDIR/etc/flirtsch/ident.mat -o ${WD}/${FreeSurferBrainMaskFile}.${FinalfMRIResolution}.nii.gz
1549-
anat_brain_mask_to_func_res = pe.Node(
1550-
interface=fsl.ApplyWarp(),
1551-
name=f"resample_anat_brain_mask_in_standard_{pipe_num}",
1628+
anat_brain_mask_to_func_res = anat_brain_mask_to_bold_res(
1629+
wf_name="anat_brain_mask_to_bold_res", cfg=cfg, pipe_num=pipe_num
15521630
)
15531631

1554-
anat_brain_mask_to_func_res.inputs.interp = "nn"
1555-
anat_brain_mask_to_func_res.inputs.premat = cfg.registration_workflows[
1556-
"anatomical_registration"
1557-
]["registration"]["FSL-FNIRT"]["identity_matrix"]
1558-
15591632
node, out = strat_pool.get_data("space-template_desc-brain_mask")
1560-
wf.connect(node, out, anat_brain_mask_to_func_res, "in_file")
1633+
wf.connect(
1634+
node,
1635+
out,
1636+
anat_brain_mask_to_func_res,
1637+
"inputspec.pace-template_desc-brain_mask",
1638+
)
15611639

15621640
wf.connect(
1563-
anat_brain_to_func_res, "out_file", anat_brain_mask_to_func_res, "ref_file"
1641+
anat_brain_to_func_res,
1642+
"outputspec.space-template_res-bold_desc-brain_T1w",
1643+
anat_brain_mask_to_func_res,
1644+
"inputspec.space-template_desc-preproc_T1w",
15641645
)
15651646

15661647
# Resample func mask in template space back to native space
@@ -1574,15 +1655,24 @@ def bold_mask_anatomical_resampled(wf, cfg, strat_pool, pipe_num, opt=None):
15741655
func_mask_template_to_native.inputs.outputtype = "NIFTI_GZ"
15751656

15761657
wf.connect(
1577-
anat_brain_mask_to_func_res, "out_file", func_mask_template_to_native, "in_file"
1658+
anat_brain_mask_to_func_res,
1659+
"outputspec.space-template_desc-bold_mask",
1660+
func_mask_template_to_native,
1661+
"in_file",
15781662
)
15791663

15801664
node, out = strat_pool.get_data("desc-preproc_bold")
15811665
wf.connect(node, out, func_mask_template_to_native, "master")
15821666

15831667
outputs = {
1584-
"space-template_res-bold_desc-brain_T1w": (anat_brain_to_func_res, "out_file"),
1585-
"space-template_desc-bold_mask": (anat_brain_mask_to_func_res, "out_file"),
1668+
"space-template_res-bold_desc-brain_T1w": (
1669+
anat_brain_to_func_res,
1670+
"outputspec.space-template_res-bold_desc-brain_T1w",
1671+
),
1672+
"space-template_desc-bold_mask": (
1673+
anat_brain_mask_to_func_res,
1674+
"outputspec.space-template_desc-bold_mask",
1675+
),
15861676
"space-bold_desc-brain_mask": (func_mask_template_to_native, "out_file"),
15871677
}
15881678

CPAC/nuisance/nuisance.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,8 +75,8 @@ def choose_nuisance_blocks(cfg, rpool, generate_only=False):
7575
]
7676
apply_transform_using = to_template_cfg["apply_transform"]["using"]
7777
input_interface = {
78-
"default": ("desc-preproc_bold", ["desc-preproc_bold", "bold"]),
79-
"abcd": ("desc-preproc_bold", "bold"),
78+
"default": ("desc-preproc_bold", ["desc-preproc_bold", "desc-reorient_bold"]),
79+
"abcd": ("desc-preproc_bold", "desc-reorient_bold"),
8080
"single_step_resampling_from_stc": ("desc-preproc_bold", "desc-stc_bold"),
8181
}.get(apply_transform_using)
8282
if input_interface is not None:

0 commit comments

Comments
 (0)