diff --git a/nipype/conftest.py b/nipype/conftest.py index 9a9175ce28..360d83cb27 100644 --- a/nipype/conftest.py +++ b/nipype/conftest.py @@ -12,11 +12,17 @@ shutil.copytree(NIPYPE_DATADIR, data_dir) +def _create_testfiles(*args): + for file in args: + open(file, 'w').close() + + @pytest.fixture(autouse=True) def add_np(doctest_namespace): doctest_namespace['np'] = numpy doctest_namespace['os'] = os doctest_namespace["datadir"] = data_dir + doctest_namespace["create_testfiles"] = _create_testfiles @pytest.fixture(autouse=True) diff --git a/nipype/interfaces/afni/__init__.py b/nipype/interfaces/afni/__init__.py index 7af80059f2..dff9e1b8f2 100644 --- a/nipype/interfaces/afni/__init__.py +++ b/nipype/interfaces/afni/__init__.py @@ -21,4 +21,4 @@ LocalBistat, Localstat, MaskTool, Merge, Notes, NwarpApply, NwarpAdjust, NwarpCat, OneDToolPy, Refit, ReHo, Resample, TCat, TCatSubBrick, TStat, To3D, Unifize, Undump, ZCutUp, GCOR, Zcat, Zeropad) -from .model import (Deconvolve, Remlfit, Synthesize) +from .model import (Deconvolve, Remlfit, Synthesize, Mema) diff --git a/nipype/interfaces/afni/model.py b/nipype/interfaces/afni/model.py index 2cccdfe869..e41ab247fa 100644 --- a/nipype/interfaces/afni/model.py +++ b/nipype/interfaces/afni/model.py @@ -13,7 +13,7 @@ import os from ..base import (CommandLineInputSpec, CommandLine, Directory, TraitedSpec, - traits, isdefined, File, InputMultiPath, Undefined, Str) + traits, isdefined, File, InputMultiObject, Undefined, Str) from ...external.due import BibTeX from .base import (AFNICommandBase, AFNICommand, AFNICommandInputSpec, @@ -21,7 +21,7 @@ class DeconvolveInputSpec(AFNICommandInputSpec): - in_files = InputMultiPath( + in_files = InputMultiObject( File(exists=True), desc='filenames of 3D+time input datasets. More than one filename can ' 'be given and the datasets will be auto-catenated in time. ' @@ -309,7 +309,7 @@ def _list_outputs(self): class RemlfitInputSpec(AFNICommandInputSpec): # mandatory files - in_files = InputMultiPath( + in_files = InputMultiObject( File(exists=True), desc='Read time series dataset', argstr='-input "%s"', @@ -356,7 +356,7 @@ class RemlfitInputSpec(AFNICommandInputSpec): '(only with \'mask\' or \'automask\' options).', argstr='-STATmask %s', exists=True) - addbase = InputMultiPath( + addbase = InputMultiObject( File( exists=True, desc='file containing columns to add to regression matrix'), @@ -367,7 +367,7 @@ class RemlfitInputSpec(AFNICommandInputSpec): copyfile=False, sep=" ", argstr='-addbase %s') - slibase = InputMultiPath( + slibase = InputMultiObject( File( exists=True, desc='file containing columns to add to regression matrix'), @@ -384,7 +384,7 @@ class RemlfitInputSpec(AFNICommandInputSpec): 'will slow the program down, and make it use a lot more memory ' '(to hold all the matrix stuff).', argstr='-slibase %s') - slibase_sm = InputMultiPath( + slibase_sm = InputMultiObject( File( exists=True, desc='file containing columns to add to regression matrix'), @@ -664,3 +664,438 @@ def _list_outputs(self): outputs[key] = os.path.abspath(self.inputs.get()[key]) return outputs + + +class MemaInputSpec(AFNICommandInputSpec): + # mandatory inputs + _subject_trait = traits.Either( + traits.Tuple( + Str(minlen=1), File(exists=True), File(exists=True) + ), + traits.Tuple( + Str(minlen=1), File(exists=True), File(exists=True), Str(''), Str('') + ) + ) + + sets = traits.List( + traits.Tuple(Str(minlen=1), traits.List(_subject_trait, minlen=1)), + desc="""\ +Specify the data for one of two test variables (either group, contrast/GLTs) A & B. + +SETNAME is the name assigned to the set, which is only for the + user's information, and not used by the program. When + there are two groups, the 1st and 2nd datasets are + associated with the 1st and 2nd labels specified + through option -set, and the group difference is + the second group minus the first one, similar to + 3dttest but different from 3dttest++. +SUBJ_K is the label for the subject K whose datasets will be + listed next +BETA_DSET is the name of the dataset of the beta coefficient or GLT. +T_DSET is the name of the dataset containing the Tstat + corresponding to BETA_DSET. + To specify BETA_DSET, and T_DSET, you can use the standard AFNI + notation, which, in addition to sub-brick indices, now allows for + the use of sub-brick labels as selectors +e.g: -set Placebo Jane pb05.Jane.Regression+tlrc'[face#0_Beta]' + pb05.Jane.Regression+tlrc'[face#0_Tstat]' +""", + argstr='-set %s...', + mandatory=True, + minlen=1, + maxlen=2, + ) + + # conditionally mandatory arguments + groups = traits.List( + Str(minlen=1), + desc="""\ +Name of 1 or 2 groups. This option must be used when comparing two groups. +Default is one group named 'G1'. The labels here are used to name the sub-bricks +in the output. When there are two groups, the 1st and 2nd labels here are associated +with the 1st and 2nd datasets specified respectively through option -set, and their +group difference is the second group minus the first one, similar to 3dttest but +different from 3dttest++.""", + argstr='-groups %s', + minlen=1, + maxlen=2, + ) + + equal_variance = traits.Bool( + True, + argstr=['-unequal_variance', '-equal_variance'], + xor=['covariates'], + desc="""\ +[-equal_variance] Assume same cross-subjects variability between GROUP1 and GROUP2 +(homoskedasticity) (Default); or [-unequal_variance] Model cross-subjects variability +difference between GROUP1 and GROUP2 (heteroskedasticity). +This option may NOT be invoked when covariate is present in the +model.""", + ) + + # Other arguments + cio = traits.Bool( + desc='use AFNIs C io functions', + argstr='-cio') + + contrast_name = traits.Str( + desc='no help available', + argstr='-contrast_name %s') + + covariates = File( + File(exists=True), + desc='Specify the name of a text file containing a table for the covariate(s).' + ' Each column in the file is treated as a separate covariate, and each row contains' + ' the values of these covariates for each subject. It is recommended to use the' + ' covariates file generated by 3dREMLfit.', + argstr='-covariates %s', + xor=['equal_variance'], + ) + + covariates_center = traits.Str( + desc="""\ +Centering rule for covariates. You can provide centering rules for each coveriate, +or specify mean centering or no centering (using 0). If no specification is made each +covariate will be centered about its own mean. +-covariates_center COV_1=CEN_1 [COV_2=CEN_2 ... ]: (for 1 group) +-covariates_center COV_1=CEN_1.A CEN_1.B [COV_2=CEN_2.A CEN_2.B ... ]: + (for 2 groups) + where COV_K is the name assigned to the K-th covariate, + either from the header of the covariates file, or from the option + -covariates_name. This makes clear which center belongs to which + covariate. When two groups are used, you need to specify a center for + each of the groups (CEN_K.A, CEN_K.B). + Example: If you had covariates age, and weight, you would use: + -covariates_center age = 78 55 weight = 165 198""", + argstr="-covariates_center %s" + ) + + covariates_model = traits.Tuple( + traits.Enum('same', 'different', desc='Specify the center'), + traits.Enum('same', 'different', desc='Specify the slope'), + desc='Specify whether to use the same or different intercepts for each of the covariates.' + ' Similarly for the slope.', + argstr='-covariates_model center=%s slope=%s' + ) + + covariates_name = traits.List( + Str(minlen=1), + desc='Specify the name of each of the N covariates. Only needed if covariate file does' + ' not have a header. Default is to name covariates cov1, cov2, ...', + argstr='-covariates_names %s') + + debugArgs = traits.Bool( + desc='Enable R to save parameters in a file called .3dMEMA.dbg.AFNI.args in the current' + ' directory for debugging.', + argstr='-dbArgs' + ) + + hk_test = traits.Bool( + desc="""\ +Perform Hartung-Knapp adjustment for the output t-statistic. \ +This approach is more robust when the number of subjects \ +is small, and is generally preferred. -KHtest is the default \ +with t-statistic output.""", + argstr=['-no_HKtest', '-HKtest'], + ) + + num_threads = traits.Int( + desc='run the program with provided number of sub-processes', + argstr='-jobs %d', + nohash=True + ) + + mask = File( + exists=True, + desc='Process voxels from inside this mask only. Default is no masking', + argstr='-mask %s' + ) + + max_zeros = traits.Range( + desc="""\ +Do not compute statistics at any voxel that has \ +more than MM zero beta coefficients or GLTs. Voxels around \ +the edges of the group brain will not have data from \ +some of the subjects. Therefore, some of their beta's or \ +GLTs and t-stats are masked with 0. 3dMEMA can handle \ +missing data at those voxels but obviously too much \ +missing data is not good. Setting -max_zeros to 0.25 \ +means process data only at voxels where no more than 1/4 \ +of the data is missing. The default value is 0 (no \ +missing values allowed). MM can be a positive integer \ +less than the number of subjects, or a fraction \ +between 0 and 1. Alternatively option -missing_data \ +can be used to handle missing data.""", + low=0.0, high=1.0, + argstr='-max_zeros %f', + xor=['missing_data'], + ) + + missing_data = traits.Either( + 0, + traits.List(File(exists=True), minlen=1, maxlen=2,), + desc="""\ +This option corrects for inflated statistics for the voxels where +some subjects do not have any data available due to imperfect +spatial alignment or other reasons. The absence of this option +means no missing data will be assumed. +0: With this format the zero value at a voxel of each subject + will be interpreted as missing data. +File1 [File2]: Information about missing data is specified + with file of 1 or 2 groups (the number 1 or 2 + and file order should be consistent with those + in option -groups). The voxel value of each file + indicates the number of sujects with missing data + in that group.""", + argstr='-missing_data %s', + xor=['max_zeros'] + ) + + outliers = traits.Bool( + False, + usedefault=True, + desc='Model outlier betas with a Laplace distribution of ' + 'subject-specific error. Default is -no_model_outliers', + argstr=['-no_model_outliers', '-model_outliers'], + ) + + nonzeros = traits.Float( + desc="""\ +Do not compute statistics at any voxel that has \ +less than NN non-zero beta values. This options is \ +complimentary to -max_zeros, and matches an option in \ +the interactive 3dMEMA mode. NN is basically (number of \ +unique subjects - MM). Alternatively option -missing_data \ +can be used to handle missing data.""", + argstr='-n_nonzero %f', + xor=['missing_data'] + ) + + residualZ = traits.Bool( + False, + usedefault=True, + desc='Output residuals and their Z values used in identifying ' + 'outliers at voxel level. Default is -no_residual_Z', + argstr=['-no_residual_Z', '-residual_Z'] + ) + + # -prefix PREFIX: Output prefix (just prefix, no view+suffix needed) + out_file = File( + desc='output dataset prefix name', + argstr='-prefix %s') + + rio = traits.Bool( + desc='use R\'s io functions', + argstr='-rio') + + verbosity = traits.Range( + value=1, + usedefault=True, + low=0, + desc='An integer specifying verbosity level. 0 is quiet, 1+ is talkative.', + argstr='-verb %d' + ) + + +class MemaOutputSpec(AFNICommandOutputSpec): + out_file = File( + desc='...', + exists=True + ) + + args = File( + desc='Arguments file for debugging, generated if -dbArgs is set') + + +class Mema(AFNICommand): + """Description of 3dMEMA + + For complete details, see the `3dMEMA Documentation. + `__ + + Examples + ======== + + .. testsetup:: + >>> create_testfiles(*[ + ... 'pb05.%s.%s.nii' % (n, c) + ... for n in ('Jane', 'John', 'Lisa', 'Amy', 'Josh', 'Mark') + ... for c in ('betas', 'tvals')]) + >>> create_testfiles('pb05.Jane.Regression+tlrc') + >>> create_testfiles('covariates.txt') + + >>> from nipype.interfaces import afni + >>> mema = afni.Mema() + >>> mema.inputs.sets = [('Placebo', [ + ... ('Jane', 'pb05.Jane.Regression+tlrc', 'pb05.Jane.Regression+tlrc', + ... '[face#0_Beta]', '[face#0_Tstat]') + ... ])] + >>> mema.cmdline + "3dMEMA -no_model_outliers -no_residual_Z -set Placebo Jane \ +pb05.Jane.Regression+tlrc'[face#0_Beta]' pb05.Jane.Regression+tlrc'[face#0_Tstat]' -verb 1" + + >>> mema.inputs.missing_data = 0 + >>> mema.cmdline + "3dMEMA -missing_data 0 -no_model_outliers -no_residual_Z -set Placebo \ +Jane pb05.Jane.Regression+tlrc'[face#0_Beta]' pb05.Jane.Regression+tlrc'[face#0_Tstat]' -verb 1" + + >>> mema.inputs.sets = [ + ... ('Placebo', [ + ... ('Jane', 'pb05.Jane.betas.nii', 'pb05.Jane.tvals.nii'), + ... ('John', 'pb05.John.betas.nii', 'pb05.John.tvals.nii'), + ... ('Lisa', 'pb05.Lisa.betas.nii', 'pb05.Lisa.tvals.nii')]), + ... ('Treatment', [ + ... ('Amy', 'pb05.Amy.betas.nii', 'pb05.Amy.tvals.nii'), + ... ('Josh', 'pb05.Josh.betas.nii', 'pb05.Josh.tvals.nii'), + ... ('Mark', 'pb05.Mark.betas.nii', 'pb05.Mark.tvals.nii')])] + >>> mema.cmdline # doctest: +IGNORE_EXCEPTION_DETAIL + Traceback (most recent call last): + ValueError: + ... + + >>> mema.inputs.groups = ['Placebo', 'Treatment'] + >>> mema.cmdline + '3dMEMA -groups Placebo Treatment \ +-missing_data 0 -no_model_outliers -no_residual_Z -set Placebo \ +Jane pb05.Jane.betas.nii pb05.Jane.tvals.nii \ +John pb05.John.betas.nii pb05.John.tvals.nii \ +Lisa pb05.Lisa.betas.nii pb05.Lisa.tvals.nii -set Treatment \ +Amy pb05.Amy.betas.nii pb05.Amy.tvals.nii \ +Josh pb05.Josh.betas.nii pb05.Josh.tvals.nii \ +Mark pb05.Mark.betas.nii pb05.Mark.tvals.nii -verb 1' + + + >>> mema = afni.Mema() + >>> mema.inputs.sets = [('Placebo', [ + ... ('Jane', 'pb05.Jane.Regression+tlrc', 'pb05.Jane.Regression+tlrc', + ... '[face#0_Beta]', '[face#0_Tstat]') + ... ])] + >>> mema.inputs.nonzeros = 18 + >>> mema.cmdline + "3dMEMA -n_nonzero 18.000000 -no_model_outliers -no_residual_Z \ +-set Placebo Jane pb05.Jane.Regression+tlrc'[face#0_Beta]' \ +pb05.Jane.Regression+tlrc'[face#0_Tstat]' \ +-verb 1" + + >>> mema.inputs.hk_test = True + >>> mema.cmdline + "3dMEMA -HKtest -n_nonzero 18.000000 -no_model_outliers \ +-no_residual_Z -set Placebo Jane pb05.Jane.Regression+tlrc'[face#0_Beta]' \ +pb05.Jane.Regression+tlrc'[face#0_Tstat]' \ +-verb 1" + + >>> mema.inputs.outliers = True + >>> mema.cmdline + "3dMEMA -HKtest -n_nonzero 18.000000 -model_outliers \ +-no_residual_Z -set Placebo Jane pb05.Jane.Regression+tlrc'[face#0_Beta]' \ +pb05.Jane.Regression+tlrc'[face#0_Tstat]' \ +-verb 1" + + >>> mema.inputs.residualZ = True + >>> mema.cmdline + "3dMEMA -HKtest -n_nonzero 18.000000 -model_outliers \ +-residual_Z -set Placebo Jane pb05.Jane.Regression+tlrc'[face#0_Beta]' \ +pb05.Jane.Regression+tlrc'[face#0_Tstat]' \ +-verb 1" + + >>> mema.inputs.equal_variance = False + >>> mema.cmdline + "3dMEMA -unequal_variance -HKtest -n_nonzero 18.000000 -model_outliers \ +-residual_Z -set Placebo Jane pb05.Jane.Regression+tlrc'[face#0_Beta]' \ +pb05.Jane.Regression+tlrc'[face#0_Tstat]' \ +-verb 1" + + >>> mema = afni.Mema() + >>> mema.inputs.sets = [('Placebo', [ + ... ('Jane', 'pb05.Jane.Regression+tlrc', 'pb05.Jane.Regression+tlrc', + ... '[face#0_Beta]', '[face#0_Tstat]') + ... ])] + >>> mema.inputs.covariates = 'covariates.txt' + >>> mema.cmdline + "3dMEMA -covariates covariates.txt \ +-no_model_outliers -no_residual_Z -set Placebo \ +Jane pb05.Jane.Regression+tlrc'[face#0_Beta]' \ +pb05.Jane.Regression+tlrc'[face#0_Tstat]' \ +-verb 1" + + >>> mema.inputs.covariates_center = 'age = 25 13 weight = 100 150' + >>> mema.cmdline + "3dMEMA -covariates covariates.txt -covariates_center age = 25 13 \ +weight = 100 150 -no_model_outliers -no_residual_Z -set Placebo \ +Jane pb05.Jane.Regression+tlrc'[face#0_Beta]' \ +pb05.Jane.Regression+tlrc'[face#0_Tstat]' \ +-verb 1" + + >>> mema.inputs.covariates_model = ('different', 'same') + >>> mema.cmdline + "3dMEMA -covariates covariates.txt -covariates_center age = 25 13 \ +weight = 100 150 -covariates_model center=different slope=same \ +-no_model_outliers -no_residual_Z -set Placebo \ +Jane pb05.Jane.Regression+tlrc'[face#0_Beta]' \ +pb05.Jane.Regression+tlrc'[face#0_Tstat]' \ +-verb 1" + + >>> mema.inputs.out_file = 'Results.BRIK' + >>> mema.cmdline + "3dMEMA -covariates covariates.txt -covariates_center age = 25 13 \ +weight = 100 150 -covariates_model center=different slope=same \ +-prefix Results.BRIK -no_model_outliers -no_residual_Z -set Placebo \ +Jane pb05.Jane.Regression+tlrc'[face#0_Beta]' \ +pb05.Jane.Regression+tlrc'[face#0_Tstat]' \ +-verb 1" + + """ + + _cmd = '3dMEMA' + input_spec = MemaInputSpec + output_spec = MemaOutputSpec + + def __init__(self, *args, **kwargs): + self._n_sets = None + return super(Mema, self).__init__(*args, **kwargs) + + def _format_arg(self, name, trait_spec, value): + if name == "sets": + formatted_values = [] + for setname, setopts in value: + formatted_subject = [] + for this_set in setopts: + if len(this_set) == 5: + subid, beta_file, ttst_file, beta_opts, ttst_opts = this_set + if beta_opts: + beta_file = "%s'%s'" % (beta_file, beta_opts) + if ttst_opts: + ttst_file = "%s'%s'" % (ttst_file, ttst_opts) + else: + subid, beta_file, ttst_file = this_set + formatted_subject.append(' '.join((subid, beta_file, ttst_file))) + formatted_values.append(' '.join([setname] + formatted_subject)) + value = formatted_values + return super(Mema, self)._format_arg(name, trait_spec, value) + + def _parse_inputs(self, skip=None): + self._n_sets = len(self.inputs.sets) + if self._n_sets == 2 and not isdefined(self.inputs.groups): + raise ValueError( + 'When two of ``-set`` are given, the option ``-groups`` must be defined.') + elif isdefined(self.inputs.groups) and len(self.inputs.groups) != self._n_sets: + raise ValueError( + 'Number of group names (``-groups``) and sets (``-set``) must match.') + + if (isdefined(self.inputs.missing_data) and isinstance(self.inputs.missing_data, list) and + len(self.inputs.missing_data) != self._n_sets): + raise ValueError( + 'Number of files given in ``-missing_data`` must match the number of sets.') + + if skip is None: + skip = [] + return super(Mema, self)._parse_inputs(skip) + + def _list_outputs(self): + outputs = self.output_spec().get() + + for key in outputs.keys(): + if isdefined(self.inputs.get()[key]): + outputs[key] = os.path.abspath(self.inputs.get()[key]) + + return outputs diff --git a/nipype/interfaces/base/core.py b/nipype/interfaces/base/core.py index 77ab6cf398..d7e38b5428 100644 --- a/nipype/interfaces/base/core.py +++ b/nipype/interfaces/base/core.py @@ -203,10 +203,10 @@ def _check_requires(self, spec, name, value): ] if any(values) and isdefined(value): if len(values) > 1: - fmt = ("%s requires values for inputs %s because '%s' is set. " + fmt = ("%s requires values for inputs %s because '%s' is set. " "For a list of required inputs, see %s.help()") else: - fmt = ("%s requires a value for input %s because '%s' is set. " + fmt = ("%s requires a value for input %s because '%s' is set. " "For a list of required inputs, see %s.help()") msg = fmt % (self.__class__.__name__, ', '.join("'%s'" % req for req in spec.requires), @@ -766,7 +766,10 @@ def _format_arg(self, name, trait_spec, value): """ argstr = trait_spec.argstr iflogger.debug('%s_%s', name, value) - if trait_spec.is_trait_type(traits.Bool) and "%" not in argstr: + if trait_spec.is_trait_type(traits.Bool) and isinstance(argstr, (list, tuple, dict)): + return argstr[value] + + elif trait_spec.is_trait_type(traits.Bool) and "%" not in argstr: # Boolean options have no format string. Just append options if True. return argstr if value else None # traits.Either turns into traits.TraitCompound and does not have any