Skip to content

Commit c4825d5

Browse files
authored
Add qsiprep/qsirecon bids app options (#11)
* qsiprep/qsirecon seem to work * ruff
1 parent 2cb8d46 commit c4825d5

File tree

6 files changed

+891
-119
lines changed

6 files changed

+891
-119
lines changed

src/simbids/cli/parser.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,7 @@ def _bids_filter(value, parser):
147147
)
148148

149149
parser.add_argument(
150-
'bids_app',
150+
'--bids-app',
151151
choices=['qsiprep', 'qsirecon', 'xcp_d', 'fmriprep'],
152152
help=('BIDS-App to be simulated'),
153153
)

src/simbids/utils/bids.py

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,75 @@
3939
from simbids.data import load as load_data
4040

4141

42+
def write_derivative_description(input_dir, output_dir):
43+
"""Write dataset_description.json file for derivatives.
44+
45+
Parameters
46+
----------
47+
input_dir : :obj:`str`
48+
Path to the primary input dataset being ingested.
49+
This may be a raw BIDS dataset (in the case of raw+derivatives workflows)
50+
or a preprocessing derivatives dataset (in the case of derivatives-only workflows).
51+
output_dir : :obj:`str`
52+
Path to the output xcp-d dataset.
53+
dataset_links : :obj:`dict`, optional
54+
Dictionary of dataset links to include in the dataset description.
55+
"""
56+
57+
DOWNLOAD_URL = 'https://github.com/nipreps/simbids/archive/0.1.0.tar.gz'
58+
59+
input_dir = Path(input_dir)
60+
output_dir = Path(output_dir)
61+
62+
orig_dset_description = os.path.join(input_dir, 'dataset_description.json')
63+
if not os.path.isfile(orig_dset_description):
64+
raise FileNotFoundError(f'Dataset description does not exist: {orig_dset_description}')
65+
66+
with open(orig_dset_description) as fobj:
67+
desc = json.load(fobj)
68+
69+
# Update dataset description
70+
desc['Name'] = 'fMRIPost-AROMA- ICA-AROMA Postprocessing Outputs'
71+
desc['BIDSVersion'] = '1.9.0dev'
72+
desc['DatasetType'] = 'derivative'
73+
desc['HowToAcknowledge'] = 'Include the generated boilerplate in the methods section.'
74+
75+
# Start with GeneratedBy from the primary input dataset's dataset_description.json
76+
desc['GeneratedBy'] = desc.get('GeneratedBy', [])
77+
78+
# Add GeneratedBy from fMRIPost-AROMA
79+
desc['GeneratedBy'].insert(
80+
0,
81+
{
82+
'Name': 'SimBIDS',
83+
'Version': '0.1.0',
84+
'CodeURL': DOWNLOAD_URL,
85+
},
86+
)
87+
88+
# Keys that can only be set by environment
89+
if 'SIMBIDS_SINGULARITY_URL' in os.environ:
90+
desc['GeneratedBy'][0]['Container'] = {
91+
'Type': 'singularity',
92+
'URI': os.getenv('SIMBIDS_SINGULARITY_URL'),
93+
}
94+
95+
# Replace local templateflow path with URL
96+
dataset_links = {}
97+
dataset_links['templateflow'] = 'https://github.com/templateflow/templateflow'
98+
99+
# Add DatasetLinks
100+
desc['DatasetLinks'] = desc.get('DatasetLinks', {})
101+
for k, v in dataset_links.items():
102+
if k in desc['DatasetLinks'].keys() and str(desc['DatasetLinks'][k]) != str(v):
103+
print(f'"{k}" is already a dataset link. Overwriting.')
104+
105+
desc['DatasetLinks'][k] = str(v)
106+
107+
out_desc = Path(output_dir / 'dataset_description.json')
108+
out_desc.write_text(json.dumps(desc, indent=4))
109+
110+
42111
def collect_derivatives(
43112
raw_dataset: Path | BIDSLayout | None,
44113
derivatives_dataset: Path | BIDSLayout | None,

src/simbids/workflows/base.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@
3636

3737
from simbids import config
3838

39+
from ..utils.bids import write_derivative_description
40+
3941

4042
def init_simbids_wf():
4143
"""Build *SimBIDS*'s pipeline.
@@ -61,7 +63,7 @@ def init_simbids_wf():
6163

6264
simbids_wf = Workflow(name=f'simbids_{ver.major}_{ver.minor}_wf')
6365
simbids_wf.base_dir = config.execution.work_dir
64-
66+
write_derivative_description(config.execution.bids_dir, config.execution.output_dir)
6567
for subject_id in config.execution.participant_label:
6668
single_subject_wf = init_single_subject_wf(subject_id)
6769

@@ -114,5 +116,13 @@ def init_single_subject_wf(subject_id: str):
114116
from simbids.workflows.qsiprep import init_single_subject_qsiprep_wf
115117

116118
return init_single_subject_qsiprep_wf(subject_id)
119+
elif config.workflow.bids_app == 'qsirecon':
120+
from simbids.workflows.qsirecon import (
121+
init_single_subject_qsirecon_wf,
122+
write_root_level_atlases,
123+
)
124+
125+
write_root_level_atlases(config.execution.output_dir)
126+
return init_single_subject_qsirecon_wf(subject_id)
117127
else:
118-
raise ValueError(f'Unknown application: {config.workflow.simulated_app}')
128+
raise ValueError(f'Unknown application: {config.workflow.bids_app}')

src/simbids/workflows/qsiprep/qsiprep.py

Lines changed: 102 additions & 116 deletions
Original file line numberDiff line numberDiff line change
@@ -313,134 +313,120 @@ def init_single_subject_qsiprep_wf(subject_id: str):
313313

314314
# Create the anatomical datasinks
315315
anatomical_template = 'MNI152NLin6Asym'
316-
for anat_file in subject_data['t1w']:
317-
workflow.add_nodes(
318-
[
319-
pe.Node(
320-
DerivativesDataSink(
321-
compress=True,
322-
in_file=anat_file,
323-
source_file=anat_file,
324-
base_directory=config.execution.output_dir,
325-
space='ACPC',
326-
desc='preproc',
327-
keep_dtype=True,
328-
),
329-
name=_get_wf_name(anat_file, 'ds_t1_preproc'),
330-
run_without_submitting=True,
316+
anat_file = (subject_data['t1w'] + subject_data['t2w'])[0]
317+
workflow.add_nodes(
318+
[
319+
pe.Node(
320+
DerivativesDataSink(
321+
compress=True,
322+
in_file=anat_file,
323+
source_file=anat_file,
324+
base_directory=config.execution.output_dir,
325+
space='ACPC',
326+
desc='preproc',
327+
keep_dtype=True,
331328
),
332-
pe.Node(
333-
DerivativesDataSink(
334-
compress=True,
335-
in_file=anat_file,
336-
source_file=anat_file,
337-
base_directory=config.execution.output_dir,
338-
space='ACPC',
339-
desc='brain',
340-
suffix='mask',
341-
),
342-
name=_get_wf_name(anat_file, 'ds_t1_mask'),
343-
run_without_submitting=True,
329+
name=_get_wf_name(anat_file, 'ds_t1_preproc'),
330+
run_without_submitting=True,
331+
),
332+
pe.Node(
333+
DerivativesDataSink(
334+
compress=True,
335+
in_file=anat_file,
336+
source_file=anat_file,
337+
base_directory=config.execution.output_dir,
338+
space='ACPC',
339+
desc='brain',
340+
suffix='mask',
344341
),
345-
pe.Node(
346-
DerivativesDataSink(
347-
compress=True,
348-
in_file=anat_file,
349-
source_file=anat_file,
350-
base_directory=config.execution.output_dir,
351-
space='ACPC',
352-
suffix='dseg',
353-
),
354-
name=_get_wf_name(anat_file, 'ds_t1_seg'),
355-
run_without_submitting=True,
342+
name=_get_wf_name(anat_file, 'ds_t1_mask'),
343+
run_without_submitting=True,
344+
),
345+
pe.Node(
346+
DerivativesDataSink(
347+
compress=True,
348+
in_file=anat_file,
349+
source_file=anat_file,
350+
base_directory=config.execution.output_dir,
351+
space='ACPC',
352+
suffix='dseg',
356353
),
357-
pe.Node(
358-
DerivativesDataSink(
359-
compress=True,
360-
in_file=anat_file,
361-
source_file=anat_file,
362-
base_directory=config.execution.output_dir,
363-
space='ACPC',
364-
desc='aseg',
365-
suffix='dseg',
366-
),
367-
name=_get_wf_name(anat_file, 'ds_t1_aseg'),
368-
run_without_submitting=True,
354+
name=_get_wf_name(anat_file, 'ds_t1_seg'),
355+
run_without_submitting=True,
356+
),
357+
pe.Node(
358+
DerivativesDataSink(
359+
compress=True,
360+
in_file=anat_file,
361+
source_file=anat_file,
362+
base_directory=config.execution.output_dir,
363+
space='ACPC',
364+
desc='aseg',
365+
suffix='dseg',
369366
),
370-
pe.Node(
371-
DerivativesDataSink(
372-
in_file=anat_file,
373-
source_file=anat_file,
374-
base_directory=config.execution.output_dir,
375-
to='ACPC',
376-
mode='image',
377-
suffix='xfm',
378-
**{'from': anatomical_template},
379-
),
380-
name=_get_wf_name(anat_file, 'ds_t1_mni_inv_warp'),
381-
run_without_submitting=True,
367+
name=_get_wf_name(anat_file, 'ds_t1_aseg'),
368+
run_without_submitting=True,
369+
),
370+
pe.Node(
371+
DerivativesDataSink(
372+
in_file=anat_file,
373+
source_file=anat_file,
374+
base_directory=config.execution.output_dir,
375+
suffix='xfm',
376+
extension='.h5',
377+
**{'from': anatomical_template, 'to': 'ACPC', 'mode': 'image'},
382378
),
383-
pe.Node(
384-
DerivativesDataSink(
385-
source_file=anat_file,
386-
in_file=anat_file,
387-
base_directory=config.execution.output_dir,
388-
to='ACPC',
389-
mode='image',
390-
suffix='xfm',
391-
**{'from': 'anat'},
392-
),
393-
name=_get_wf_name(anat_file, 'ds_t1_template_acpc_transforms'),
394-
run_without_submitting=True,
379+
name=_get_wf_name(anat_file, 'ds_t1_mni_inv_warp'),
380+
run_without_submitting=True,
381+
),
382+
pe.Node(
383+
DerivativesDataSink(
384+
source_file=anat_file,
385+
in_file=text_file,
386+
base_directory=config.execution.output_dir,
387+
to='ACPC',
388+
mode='image',
389+
suffix='xfm',
390+
extension='.mat',
391+
**{'from': 'anat'},
395392
),
396-
pe.Node(
397-
DerivativesDataSink(
398-
in_file=anat_file,
399-
source_file=anat_file,
400-
base_directory=config.execution.output_dir,
401-
to='anat',
402-
mode='image',
403-
suffix='xfm',
404-
**{'from': 'ACPC'},
405-
),
406-
name=_get_wf_name(anat_file, 'ds_t1_template_acpc_inv_transforms'),
407-
run_without_submitting=True,
393+
name=_get_wf_name(anat_file, 'ds_t1_template_acpc_transforms'),
394+
run_without_submitting=True,
395+
),
396+
pe.Node(
397+
DerivativesDataSink(
398+
in_file=text_file,
399+
source_file=anat_file,
400+
base_directory=config.execution.output_dir,
401+
to='anat',
402+
mode='image',
403+
suffix='xfm',
404+
extension='.mat',
405+
**{'from': 'ACPC'},
408406
),
409-
pe.Node(
410-
DerivativesDataSink(
411-
in_file=anat_file,
412-
source_file=anat_file,
413-
base_directory=config.execution.output_dir,
414-
to=anatomical_template,
415-
mode='image',
416-
suffix='xfm',
417-
**{'from': 'ACPC'},
418-
),
419-
name=_get_wf_name(anat_file, 'ds_t1_mni_warp'),
420-
run_without_submitting=True,
407+
name=_get_wf_name(anat_file, 'ds_t1_template_acpc_inv_transforms'),
408+
run_without_submitting=True,
409+
),
410+
pe.Node(
411+
DerivativesDataSink(
412+
in_file=anat_file,
413+
source_file=anat_file,
414+
base_directory=config.execution.output_dir,
415+
to=anatomical_template,
416+
mode='image',
417+
suffix='xfm',
418+
extension='.h5',
419+
**{'from': 'ACPC'},
421420
),
422-
]
423-
)
421+
name=_get_wf_name(anat_file, 'ds_t1_mni_warp'),
422+
run_without_submitting=True,
423+
),
424+
]
425+
)
424426

425427
return clean_datasinks(workflow)
426428

427429

428-
def init_single_dwi_run_wf(dwi_file: str):
429-
"""Set up a single-run workflow for SimBIDS."""
430-
from niworkflows.engine.workflows import LiterateWorkflow as Workflow
431-
432-
workflow = Workflow(name=_get_wf_name(dwi_file, 'single_run'))
433-
workflow.__desc__ = ''
434-
435-
# Fill in datasinks seen so far
436-
for node in workflow.list_node_names():
437-
if node.split('.')[-1].startswith('ds_'):
438-
workflow.get_node(node).inputs.base_directory = config.execution.output_dir
439-
workflow.get_node(node).inputs.source_file = dwi_file
440-
441-
return workflow
442-
443-
444430
def clean_datasinks(workflow: pe.Workflow) -> pe.Workflow:
445431
"""Overwrite ``out_path_base`` of DataSinks."""
446432
for node in workflow.list_node_names():
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
from .qsirecon import init_single_subject_qsirecon_wf, write_root_level_atlases
2+
3+
__all__ = ['init_single_subject_qsirecon_wf', 'write_root_level_atlases']

0 commit comments

Comments
 (0)