nipy / nipype

Workflows and interfaces for neuroimaging packages
https://nipype.readthedocs.org/en/latest/
Other
751 stars 530 forks source link

nipype.algorithms.modelgen.SpecifySPMModel can't handle relative filepaths when being inside a node #3564

Open JohannesWiesner opened 1 year ago

JohannesWiesner commented 1 year ago

Summary

This issue is potentially related to #3301. I would like to use nipype.algorithms.modelgen.SpecifySPMModel to set up my design matrix. I noticed that the class itself is able to handle relative filepaths for its argument functional_runs, but not when being inside a node. Is this an expected behavior?

How to replicate the behavior

import nipype.interfaces.matlab as nim
from nilearn.datasets import fetch_spm_auditory
from nilearn.image import concat_imgs
from nipype.algorithms import modelgen
from nipype.interfaces.base import Bunch
from nipype import Node
from nipype.algorithms.modelgen import SpecifySPMModel
import os

mlab = nim.MatlabCommand()
nim.MatlabCommand.set_default_matlab_cmd('/opt/matlab/R2022a/bin/matlab')

# get data (this will automatically create a separate data directory)
subject_data = fetch_spm_auditory(data_dir='./data')

# create .nii file and save to data dir. We pretend this would be two different runs
fmri_img = concat_imgs(subject_data.func)
fmri_img.to_filename('./data/functional2.nii')
fmri_img.to_filename('./data/functional3.nii')

# taken from docs: https://nipype.readthedocs.io/en/latest/api/generated/nipype.algorithms.modelgen.html#specifyspmmodel
s = modelgen.SpecifySPMModel()
s.inputs.input_units = 'secs'
s.inputs.output_units = 'scans'
s.inputs.high_pass_filter_cutoff = 128.
s.inputs.functional_runs = ['./data/functional2.nii', './data/functional3.nii']
s.inputs.time_repetition = 6
s.inputs.concatenate_runs = True
evs_run2 = Bunch(conditions=['cond1'], onsets=[[2, 50, 100, 180]], durations=[[1]])
evs_run3 = Bunch(conditions=['cond1'], onsets=[[30, 40, 100, 150]], durations=[[1]])
s.inputs.subject_info = [evs_run2, evs_run3]
s.run()

# but when we put the same code into a node it will fail
model_specifier = Node(SpecifySPMModel(input_units='secs',
                                       output_units='scans',
                                       high_pass_filter_cutoff=128,
                                       functional_runs = ['./data/functional2.nii', './data/functional3.nii'],
                                       time_repetition = 6,
                                       concatenate_runs = True,
                                       subject_info=[evs_run2, evs_run3]),
                       name='model_specifier')

model_specifier.run()

Actual behavior

model_specifier.run() results in TraitError: Each element of the 'functional_runs' trait of a SpecifySPMModelInputSpec instance must be a list of items which are a pathlike object or string representing an existing file or a pathlike object or string representing an existing file, but a value of '/tmp/tmprbnvs25y/model_specifier/functional2.nii' <class 'str'> was specified.

Expected behavior

As a user I would expect that model_specifier.run() should run successfully just like s.run() , because the only difference between the first and the second case is that the latter is packed inside a Node, everything else is the same. One can fix this by Node(SpecifySPMModel(functional_runs=...) with absolute paths like such:

functional_runs = [os.path.join(os.getcwd(),'./data/functional2.nii'),
                   os.path.join(os.getcwd(),'./data/functional3.nii')]

But as a user I would not expect to do this,

Script/Workflow details

Please put URL to code or code here (if not too long).

Platform details:

{'commit_hash': '%h',
 'commit_source': 'archive substitution',
 'networkx_version': '3.0',
 'nibabel_version': '5.0.1',
 'nipype_version': '1.8.5',
 'numpy_version': '1.23.5',
 'pkg_path': '/home/johannes.wiesner/.conda/envs/csp_wiesner_johannes/lib/python3.9/site-packages/nipype',
 'scipy_version': '1.10.1',
 'sys_executable': '/home/johannes.wiesner/.conda/envs/csp_wiesner_johannes/bin/python',
 'sys_platform': 'linux',
 'sys_version': '3.9.16 | packaged by conda-forge | (main, Feb  1 2023, '
                '21:39:03) \n'
                '[GCC 11.3.0]',
 'traits_version': '6.4.1'}

Execution environment

Choose one

effigies commented 1 year ago

Yes. One of the functions of Nodes is isolation so that your CWD doesn't influence the behavior of tools. You will want to use absolute paths here.

JohannesWiesner commented 1 year ago

@effigies Alright thanks! I wonder if one could adapt the Error message for this case as tmp/tmprbnvs25y/model_specifier/functional2.nii is a symbolic link pointing to an existing file so I couldn't really understanding what's going on.

JohannesWiesner commented 1 year ago

Perhaps one could implement a method within the Node() class that checks if the provided paths are absolute or relative and throw an error for the latter case. One could use os.path.isabs() for example.

effigies commented 1 year ago

Relative paths are frequently used for output filenames. I don't think we could make it an error without breaking that use case.

You could dig through the traceback and find where we make the inputs absolute, and put that in a try/catch block and provide a better error message than the more opaque TraitError.

I should note though that Nipype 1 is in maintenance mode and it will probably die by the time Python 3.11 hits end-of-life. In my opinion, a better use of time would be to work to improve Pydra and write Tasks (which replaces Interfaces and Nodes) to cover your use cases.