Skip to content

Commit f28b58f

Browse files
committed
ENH: Update confounds workflow condition
1 parent 3ae01a3 commit f28b58f

File tree

3 files changed

+66
-7
lines changed

3 files changed

+66
-7
lines changed

petprep/workflows/pet/base.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -756,7 +756,7 @@ def init_pet_wf(
756756
(pet_ref_tacs_wf, ds_ref_tacs, [('outputnode.timeseries', 'in_file')]),
757757
]) # fmt:skip
758758

759-
if nvols > 1: # run these only if 4-D PET
759+
if nvols > 2: # run these only if PET has at least 3 frames
760760
pet_confounds_wf = init_pet_confs_wf(
761761
mem_gb=mem_gb['largemem'],
762762
metadata=all_metadata[0],
@@ -832,6 +832,11 @@ def _last(inlist):
832832
]),
833833
]) # fmt:skip
834834

835+
else:
836+
config.loggers.workflow.warning(
837+
'PET confounds will be skipped - series has only %d frame(s)', nvols
838+
)
839+
835840
# Fill-in datasinks of reportlets seen so far
836841
for node in workflow.list_node_names():
837842
if node.split('.')[-1].startswith('ds_report'):

petprep/workflows/pet/confounds.py

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -243,7 +243,11 @@ def init_pet_confs_wf(
243243

244244
# DVARS
245245
dvars = pe.Node(
246-
nac.ComputeDVARS(save_nstd=True, save_std=True, remove_zerovariance=True),
246+
niu.Function(
247+
function=_compute_dvars,
248+
input_names=['pet', 'mask'],
249+
output_names=['out_nstd', 'out_std'],
250+
),
247251
name='dvars',
248252
mem_gb=mem_gb,
249253
)
@@ -351,8 +355,8 @@ def init_pet_confs_wf(
351355

352356
workflow.connect([
353357
# connect inputnode to each non-anatomical confound node
354-
(inputnode, dvars, [('pet', 'in_file'),
355-
('pet_mask', 'in_mask')]),
358+
(inputnode, dvars, [('pet', 'pet'),
359+
('pet_mask', 'mask')]),
356360
(inputnode, motion_params, [('motion_xfm', 'xfm_file'),
357361
('petref', 'petref_file')]),
358362
(inputnode, rmsd, [('motion_xfm', 'xfm_file'),
@@ -559,6 +563,29 @@ def init_carpetplot_wf(
559563
return workflow
560564

561565

566+
def _compute_dvars(pet, mask):
567+
"""Compute DVARS only when the timeseries has at least three frames."""
568+
import numpy as np
569+
import nibabel as nb
570+
from pathlib import Path
571+
from nipype.algorithms import confounds as nac
572+
573+
nvols = nb.load(pet).shape[-1]
574+
if nvols < 3:
575+
data = np.zeros((nvols,), dtype=float)
576+
out_nstd = Path('dvars.nstd.tsv').absolute()
577+
out_std = Path('dvars.std.tsv').absolute()
578+
np.savetxt(out_nstd, data)
579+
np.savetxt(out_std, data)
580+
return str(out_nstd), str(out_std)
581+
582+
dvars = nac.ComputeDVARS(save_nstd=True, save_std=True, remove_zerovariance=True)
583+
dvars.inputs.in_file = pet
584+
dvars.inputs.in_mask = mask
585+
res = dvars.run()
586+
return res.outputs.out_nstd, res.outputs.out_std
587+
588+
562589
def _binary_union(mask1, mask2):
563590
"""Generate the union of two masks."""
564591
from pathlib import Path

petprep/workflows/pet/tests/test_confounds.py

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ def test_dvars_connects_pet_mask(tmp_path):
1515
)
1616

1717
edge = wf._graph.get_edge_data(wf.get_node('inputnode'), wf.get_node('dvars'))
18-
assert ('pet_mask', 'in_mask') in edge['connect']
18+
assert ('pet_mask', 'mask') in edge['connect']
1919

2020
img = nb.Nifti1Image(np.random.rand(2, 2, 2, 5), np.eye(4))
2121
mask = nb.Nifti1Image(np.ones((2, 2, 2), dtype=np.uint8), np.eye(4))
@@ -26,9 +26,36 @@ def test_dvars_connects_pet_mask(tmp_path):
2626

2727
node = wf.get_node('dvars')
2828
node.base_dir = tmp_path
29-
node.inputs.in_file = str(pet_file)
30-
node.inputs.in_mask = str(mask_file)
29+
node.inputs.pet = str(pet_file)
30+
node.inputs.mask = str(mask_file)
3131
result = node.run()
3232

3333
assert result.outputs.out_nstd
3434
assert result.outputs.out_std
35+
36+
37+
def test_dvars_short_series(tmp_path):
38+
"""DVARS returns empty outputs when fewer than three frames."""
39+
wf = init_pet_confs_wf(
40+
mem_gb=0.01,
41+
metadata={},
42+
regressors_all_comps=False,
43+
regressors_dvars_th=1.5,
44+
regressors_fd_th=0.5,
45+
)
46+
47+
img = nb.Nifti1Image(np.random.rand(2, 2, 2, 2), np.eye(4))
48+
mask = nb.Nifti1Image(np.ones((2, 2, 2), dtype=np.uint8), np.eye(4))
49+
pet_file = tmp_path / 'pet.nii.gz'
50+
mask_file = tmp_path / 'mask.nii.gz'
51+
img.to_filename(pet_file)
52+
mask.to_filename(mask_file)
53+
54+
node = wf.get_node('dvars')
55+
node.base_dir = tmp_path
56+
node.inputs.pet = str(pet_file)
57+
node.inputs.mask = str(mask_file)
58+
result = node.run()
59+
60+
assert np.loadtxt(result.outputs.out_nstd).sum() == 0
61+
assert np.loadtxt(result.outputs.out_std).sum() == 0

0 commit comments

Comments
 (0)