<commit_before>"""The spm module provides basic functions for interfacing with matlab
and spm to access spm tools.

These functions include:
    
* Realign: within-modality registration

* Coregister: between modality registration
    
* Normalize: non-linear warping to standard space

* Segment: bias correction, segmentation

* Smooth: smooth with Gaussian kernel

"""
__docformat__ = 'restructuredtext'

# Standard library imports
import os
from glob import glob
from copy import deepcopy
import re

# Third-party imports
import numpy as np
from scipy.io import savemat

# Local imports
from nipype.interfaces.base import Bunch, InterfaceResult, Interface
from nipype.utils import setattr_on_read
from nipype.externals.pynifti import load
from nipype.interfaces.matlab import MatlabCommandLine
from nipype.utils.filemanip import (fname_presuffix, fnames_presuffix, 
                                    filename_to_list, list_to_filename,
                                    loadflat)
from nipype.utils.spm_docs import grab_doc
import logging
logger = logging.getLogger('spmlogger')

def scans_for_fname(fname):
    """Reads a nifti file and converts it to a numpy array storing
    individual nifti volumes
    
    Opens images so will fail if they are not found
    """
    if isinstance(fname,list):
        scans = np.zeros((len(fname),),dtype=object)
        for sno,f in enumerate(fname):
            scans[sno] = '%s,1'%f
        return scans
    img = load(fname)
    if len(img.get_shape()) == 3:
        return np.array(('%s,1'%fname,),dtype=object)
    else:
        n_scans = img.get_shape()[3]
        scans = np.zeros((n_scans,),dtype=object)
        for sno in range(n_scans):
            scans[sno] = '%s,%d'% (fname, sno+1)
        return scans

def scans_for_fnames(fnames,keep4d=False,separate_sessions=False):
    """Converts a list of files to a concatenated numpy array for each
    volume.

    keep4d : boolean
        keeps the entries of the numpy array as 4d files instead of
        extracting the individual volumes.
    separate_sessions: boolean
        if 4d nifti files are being used, then separate_sessions
        ensures a cell array per session is created in the structure.
    """
    flist = None
    if separate_sessions or keep4d:
        flist = np.zeros((len(fnames),),dtype=object)
    for i,f in enumerate(fnames):
        if separate_sessions:
            if keep4d:
                flist[i] = np.array([f],dtype=object)
            else:
                flist[i] = scans_for_fname(f)
        else:
            if keep4d:
                flist[i] = f
            else:
                scans = scans_for_fname(f)
                if flist is None:
                    flist = scans
                else:
                    flist = np.concatenate((flist,scans))
    return flist

class SpmInfo(object):
    """ Return the path to the spm directory in the matlab path
        If path not found, prints error asn returns None
    """
    __path = None
    @classmethod
    def spm_path(cls):
        if cls.__path == None:
            mlab = MatlabCommandLine()
            mlab.inputs.script_name = 'spminfo'
            mlab.inputs.script_lines = """
    if isempty(which('spm')), throw(MException('SPMCheck:NotFound','SPM not in matlab path'));end;
    spm_path = spm('dir');
    fprintf(1, '<PATH>%s</PATH>', spm_path);
    """
            mlab.inputs.mfile = False
            out = mlab.run()
            if out.runtime.returncode == 0:
                path = re.match('<PATH>(.*)</PATH>',out.runtime.stdout[out.runtime.stdout.find('<PATH>'):])
                if path is not None:
                    path = path.groups()[0]
                cls.__path = path
            else:
                print out.runtime.stderr
                return None
            
        return cls.__path

class SpmMatlabCommandLine(MatlabCommandLine):
    """ Extends the `MatlabCommandLine` class to handle SPM specific
    formatting of matlab scripts.
    """

    opt_map = {}

    def __init__(self, matlab_cmd=None, **inputs): 
        super(SpmMatlabCommandLine,self).__init__(**inputs)
        self.mfile = True

    @property
    def jobtype(self):
        """the jobtype used by spm/matlab
        to specify the jobtype to run
        jobs{1}.jobtype{1}.jobname"""
        return 'jobtype' 

    @property
    def jobname(self):
        """the jobname used by spm/matlab
        to specify the jobname to run
        jobs{1}.jobtype{1}.jobname"""
        return 'jobname' 

    def _use_mfile(self, use_mfile):
        """boolean,
        if true generates a matlab <filename>.m file
        if false generates a binary .mat file
        """
        self.mfile = use_mfile

    def run(self, **inputs):
        """Executes the SPM function using MATLAB
        """
        results = super(SpmMatlabCommandLine,self).run()
        if results.runtime.returncode == 0:
            results.outputs = self.aggregate_outputs() 
        return results

    def _populate_inputs(self):
        self.inputs = Bunch()
        for k,v in self.opt_map.items():
            if len(v)<3:
                setattr(self.inputs, k, None)
            else:
                setattr(self.inputs, k, v[2])
        #self.inputs = Bunch((k,None) for k in self.opt_map.keys())

    @classmethod
    def help(cls):
        cls.inputs_help()
        print ''
        cls.outputs_help()
        
    @classmethod
    def inputs_help(cls):
        helpstr = ['Inputs','------']
        opthelpstr = None
        manhelpstr = None
        for k,v in sorted(cls.opt_map.items()):
            if '(opt' in v[1]:
                if not opthelpstr:
                    opthelpstr = ['','Optional:']
                opthelpstr += ['%s: %s'%(k,v[1])]
            else:
                if not manhelpstr:
                    manhelpstr = ['','Mandatory:']
                manhelpstr += ['%s: %s'%(k,v[1])]
        if manhelpstr:
            helpstr += manhelpstr
        if opthelpstr:
            helpstr += opthelpstr
        print '\n'.join(helpstr)
        #return (helpstr,manhelpstr,opthelpstr)
        
    def _convert_inputs(self, opt, val):
        """Convert input to appropriate format for spm
        """
        return val
    
    def _parse_inputs(self, skip=()):
        spmdict = {}
        inputs = sorted((k, v) for k, v in self.inputs.items()
                            if v is not None and k not in skip)
        for opt, value in inputs:
            try:
                argstr = self.opt_map[opt][0]
                if '.' in argstr:
                    fields = argstr.split('.')
                    if fields[0] not in spmdict.keys():
                        spmdict[fields[0]] = {}
                    spmdict[fields[0]][fields[1]] = self._convert_inputs(opt,value)
                else:
                    spmdict[argstr] = self._convert_inputs(opt,value)
            except KeyError:
                logger.warn("Option '%s' is not supported!" % (opt))
                raise
        return [spmdict]

    @classmethod
    def outputs_help(cls):
        """ Prints the help of outputs
        """
        helpstr = ['Outputs','-------']
        for k,v in sorted(cls.out_map.items()):
            helpstr += ['%s: %s'%(k,v[0])]
        print '\n'.join(helpstr)

    def outputs(self):
        """
        """
        outputs = Bunch()
        for k in self.out_map.keys():
            setattr(outputs, k, None)
        return outputs
    
    def _compile_command(self):
        """Assembles the matlab code for SPM function
        
        Virtual function that needs to be implemented by the
        subclass
        """ 
        self._cmdline, mscript = self._make_matlab_command(deepcopy(self._parse_inputs()))
    
    def aggregate_outputs(self):
        """Collects all the outputs produced by an SPM function
        
        Virtual function that needs to be implemented by the
        subclass to collate outputs created generated by the SPM
        functionality being wrapped.
        """ 
        raise NotImplementedError
    
    def _reformat_dict_for_savemat(self, contents):
        """Encloses a dict representation within hierarchical lists.

        In order to create an appropriate SPM job structure, a Python
        dict storing the job needs to be modified so that each dict
        embedded in dict needs to be enclosed as a list element.

        Examples
        --------
        >>> a = SpmMatlabCommandLine()._reformat_dict_for_savemat(dict(a=1,b=dict(c=2,d=3)))
        >>> print a
        [{'a': 1, 'b': [{'c': 2, 'd': 3}]}]
        
        .. notes: XXX Need to talk to Matthew about cleaning up this code.
        """
        # XXX TODO Satra, I didn't change the semantics, but got rid of extraneous stuff.
        # This seems weird.  Please have a look and make sure you intend to
        # discard empty dicts. -DJC
        # if dict is empty, SPM falls back on defaults
        # ...so empty dicts are discarded -CM
        newdict = {}
        try:
            for key, value in contents.items():
                if isinstance(value, dict):
                    if value:
                        newdict[key] = self._reformat_dict_for_savemat(value)
                    # if value is None, skip
                else:
                    newdict[key] = value
                
            return [newdict]
        except TypeError:
            print 'Requires dict input'

    def _generate_job(self, prefix='', contents=None):
        """ Recursive function to generate spm job specification as a string

        Parameters
        ----------
        prefix : string
            A string that needs to get  
        contents : dict
            A non-tuple Python structure containing spm job
            information gets converted to an appropriate sequence of
            matlab commands.
        """
        jobstring = ''
        if contents is None:
            return jobstring
        if isinstance(contents, list):
            for i,value in enumerate(contents):
                newprefix = "%s(%d)" % (prefix, i+1)
                jobstring += self._generate_job(newprefix, value)
            return jobstring
        if isinstance(contents, dict):
            for key,value in contents.items():
                newprefix = "%s.%s" % (prefix, key)
                jobstring += self._generate_job(newprefix, value)
            return jobstring
        if isinstance(contents, np.ndarray):
            if contents.dtype == np.dtype(object):
                if prefix:
                    jobstring += "%s = {...\n"%(prefix)
                else:
                    jobstring += "{...\n"
                for i,val in enumerate(contents):
                    if isinstance(val, np.ndarray):
                        jobstring += self._generate_job(prefix=None,
                                                        contents=val)
                    elif isinstance(val,str):
                        jobstring += '\'%s\';...\n'%(val)
                    else:
                        jobstring += '%s;...\n'%str(val)
                jobstring += '};\n'
            else:
                for i,val in enumerate(contents):
                    for field in val.dtype.fields:
                        if prefix:
                            newprefix = "%s(%d).%s"%(prefix, i+1, field)
                        else:
                            newprefix = "(%d).%s"%(i+1, field)
                        jobstring += self._generate_job(newprefix,
                                                        val[field])
            return jobstring
        if isinstance(contents, str):
            jobstring += "%s = '%s';\n" % (prefix,contents)
            return jobstring
        jobstring += "%s = %s;\n" % (prefix,str(contents))
        return jobstring
    
    def _make_matlab_command(self, contents, cwd=None, postscript=None):
        """ generates a mfile to build job structure
        Parameters
        ----------

        contents : list
            a list of dicts generated by _parse_inputs
            in each subclass

        cwd : string
            default os.getcwd()

        Returns
        -------
        cmdline : string
            string representing command passed to shell
            as generated by matlab.MatlabCommandLine()._gen_matlab_command

        mscript : string
            contents of a script called by matlab
        """
        if not cwd:
            cwd = os.getcwd()
        mscript  = """
        %% Generated by nipype.interfaces.spm
        if isempty(which('spm')),
             throw(MException('SPMCheck:NotFound','SPM not in matlab path'));
        end
        fprintf('SPM version: %s\\n',spm('ver'));
        fprintf('SPM path: %s\\n',which('spm'));
        spm_defaults;
                  
        if strcmp(spm('ver'),'SPM8'), spm_jobman('initcfg');end\n
        """
        if self.mfile:
            if self.jobname in ['st','smooth','preproc','fmri_spec','fmri_est'] :
                mscript += self._generate_job('jobs{1}.%s{1}.%s(1)' % 
                                             (self.jobtype,self.jobname), contents[0])
            else:
                mscript += self._generate_job('jobs{1}.%s{1}.%s{1}' % 
                                             (self.jobtype,self.jobname), contents[0])
        else:
            jobdef = {'jobs':[{self.jobtype:[{self.jobname:self.reformat_dict_for_savemat
                                         (contents[0])}]}]}
            savemat(os.path.join(cwd,'pyjobs_%s.mat'%self.jobname), jobdef)
            mscript += "load pyjobs_%s;\n\n" % self.jobname
        mscript += """ 
        if strcmp(spm('ver'),'SPM8'), 
           jobs=spm_jobman('spm5tospm8',{jobs});
        end 
        spm_jobman(\'run\',jobs);\n
        """
        if postscript is not None:
            mscript += postscript
        cmdline = self._gen_matlab_command(mscript, cwd=cwd,
                                           script_name='pyscript_%s' % self.jobname,
                                           mfile=self.mfile) 
        return cmdline, mscript


class SliceTiming(SpmMatlabCommandLine):
    """Use spm to perform slice timing correction.

    See SliceTiming().spm_doc() for more information.

    Examples
    --------

    >>> from nipype.interfaces.spm import SliceTiming
    >>> st = SliceTiming()
    >>> st.inputs.infile = 'func.nii'
    >>> st.inputs.num_slices = 32
    >>> st.inputs.time_repetition = 6.0
    >>> st.inputs.time_acquisition = 6. - 6./32.
    >>> st.inputs.slice_order = range(32,0,-1)
    >>> st.inputs.ref_slice = 1
    """

    def spm_doc(self):
        """Print out SPM documentation."""
        print grab_doc('SliceTiming')
    
    @property
    def cmd(self):
        return 'spm_st'

    @property
    def jobtype(self):
        return 'temporal'

    @property
    def jobname(self):
        return 'st'

    opt_map = {'infile': ('scans',
                          'list of filenames to apply slice timing'),
               'num_slices': ('nslices',
                              'number of slices in a volume'),
               'time_repetition': ('tr',
                                   'time between volume acquisitions ' \
                                       '(start to start time)'),
               'time_acquisition': ('ta',
                                    'time of volume acquisition. usually ' \
                                        'calculated as TR-(TR/num_slices)'),
               'slice_order': ('so',
                               '1-based order in which slices are acquired'),
               'ref_slice': ('refslice',
                             '1-based Number of the reference slice')
               }
        
    def get_input_info(self):
        """ Provides information about inputs as a dict
            info = [Bunch(key=string,copy=bool,ext='.nii'),...]
        """
        info = [Bunch(key='infile',copy=False)]
        return info

    def _convert_inputs(self, opt, val):
        """Convert input to appropriate format for spm
        """
        if opt == 'infile':
            return scans_for_fnames(filename_to_list(val),
                                    separate_sessions=True)
        return val

    def run(self, infile=None, **inputs):
        """Executes the SPM slice timing function using MATLAB
        
        Parameters
        ----------
        
        infile: string, list
            image file(s) to smooth
        """
        if infile:
            self.inputs.infile = infile
        if not self.inputs.infile:
            raise AttributeError('Slice timing requires a file')
        self.inputs.update(**inputs)
        return super(SliceTiming,self).run()

    out_map = {'timecorrected_files' : ('slice time corrected files','infile')}
        
    def aggregate_outputs(self):
        outputs = self.outputs()
        outputs.timecorrected_files = []
        filelist = filename_to_list(self.inputs.infile)
        for f in filelist:
            s_file = glob(fname_presuffix(f, prefix='a',
                                          suffix='.nii',use_ext=False))
            assert len(s_file) == 1, 'No slice time corrected file generated by SPM Slice Timing'
            outputs.timecorrected_files.append(s_file[0])
        return outputs
    
class Realign(SpmMatlabCommandLine):
    """Use spm_realign for estimating within modality rigid body alignment

    Examples
    --------

    >>> import nipype.interfaces.spm as spm
    >>> realign = spm.Realign()
    >>> realign.inputs.infile = 'a.nii'
    >>> realign.inputs.register_to_mean = True
    >>> realign.run() # doctest: +SKIP

    """

    def spm_doc(self):
        """Print out SPM documentation."""
        print grab_doc('Realign: Estimate & Reslice')

    @property
    def cmd(self):
        return 'spm_realign'

    @property
    def jobtype(self):
         return 'spatial'

    @property
    def jobname(self):
        return 'realign'

    opt_map = {'infile': ('data', 'list of filenames to realign'),
               'jobtype': (None, 'one of: estimate, write, estwrite (opt, estwrite)', 'estwrite'),
               'quality': ('eoptions.quality',
                           '0.1 = fast, 1.0 = precise (opt, 0.9)'),
               'fwhm': ('eoptions.fwhm',
                        'gaussian smoothing kernel width (opt, 5)'),
               'separation': ('eoptions.sep',
                              'sampling separation in mm (opt, 4))'),
               'register_to_mean': ('eoptions.rtm', 'True/False (opt, False)'),
               'weight_img': ('eoptions.weight',
                              'filename of weighting image (opt, None)'),
               'interp': ('eoptions.interp',
                          'degree of b-spline used for interpolation (opt, 2)'),
               'wrap': ('eoptions.wrap',
                        'Check if interpolation should wrap in [x,y,z] (opt, [0,0,0])'),
               'write_which': ('roptions.which',
                               'determines which images to reslice (opt, [2, 1])'),
               'write_interp': ('roptions.interp',
                           'degree of b-spline used for interpolation (opt, 4)'),
               'write_wrap': ('roptions.wrap',
                        'Check if interpolation should wrap in [x,y,z] (opt, [0,0,0])'),
               'write_mask': ('roptions.mask', 'True/False mask output image (opt,)')
               }

    def get_input_info(self):
        """ Provides information about inputs
            info = [Bunch(key=string,copy=bool,ext='.nii'),...]
        """
        info = [Bunch(key='infile',copy=True)]
        return info

    def _convert_inputs(self, opt, val):
        """Convert input to appropriate format for spm
        """
        if opt == 'infile':
            return scans_for_fnames(filename_to_list(val),
                                    keep4d=True,
                                    separate_sessions=True)
        if opt == 'register_to_mean': # XX check if this is necessary
            return int(val)
        if opt in ['wrap','write_wrap']:
            if len(val) != 3:
                raise ValueError('%s must have 3 elements'%opt)
        if opt == 'write_which':
            if len(val) != 2:
                raise ValueError('write_which must have 2 elements')
        return val
    
    def _parse_inputs(self):
        """validate spm realign options if set to None ignore
        """
        einputs = super(Realign, self)._parse_inputs(skip=('jobtype'))
        jobtype =  self.inputs.jobtype
        return [{'%s'%(jobtype):einputs[0]}]

    out_map = {'realigned_files' : ('Realigned files',),
               'mean_image' : ('Mean image file from the realignment',),
               'realignment_parameters' : ('Estimated translation and rotation parameters',)
               }

    def run(self, infile=None,**inputs):
        """Executes the SPM realign function using MATLAB
        
        Parameters
        ----------
        
        infile: string, list
            list of filenames to realign
        """
        if infile:
            self.inputs.infile = infile
        if not self.inputs.infile:
            raise AttributeError('Realign requires an input file')
        self.inputs.update(**inputs)
        # The following line should call the run function of Realign's
        # parent class.
        return super(Realign,self).run()
    
    def aggregate_outputs(self):
        """ Initializes the output fields for this interface and then
        searches for and stores the data that go into those fields.
        """
        outputs = self.outputs()
        outputs.realigned_files = []
        outputs.realignment_parameters = []
        filelist = filename_to_list(self.inputs.infile)
        outputs.mean_image = glob(fname_presuffix(filelist[0],prefix='mean'))[0]
        for f in filelist:
            r_file = glob(fname_presuffix(f, prefix='r', suffix='.nii',
                                          use_ext=False))
            assert len(r_file) == 1, 'No realigned file generated by SPM Realign'
            outputs.realigned_files.append(r_file[0])
            rp_file = glob(fname_presuffix(f,prefix='rp_',suffix='.txt',use_ext=False))
            assert len(rp_file) == 1, 'No realignment parameter file generated by SPM Realign'
            outputs.realignment_parameters.append(rp_file[0])
        outputs.realigned_files = list_to_filename(outputs.realigned_files)
        return outputs

class Coregister(SpmMatlabCommandLine):
    """Use spm_coreg for estimating cross-modality rigid body alignment

    Examples
    --------
    
    >>> import nipype.interfaces.spm as spm
    >>> coreg = spm.Coregister()
    >>> coreg.inputs.infile = 'a.nii'
    >>> coreg.inputs.register_to_mean = True
    >>> coreg.run() # doctest: +SKIP
    
    """
    
    def spm_doc(self):
        """Print out SPM documentation."""
        print grab_doc('Coreg: Estimate & Reslice')

    @property
    def cmd(self):
        return 'spm_coreg'

    @property
    def jobtype(self):
        return 'spatial'

    @property
    def jobname(self):
        return 'coreg'
        
    opt_map = {'target': ('ref', 'reference file to register to'),
               'source': ('source', 'file to register to target'),
               'jobtype': (None, 'one of: estimate, write, estwrite (opt,estwrite)','estwrite'),
               'apply_to_files': ('other', 'files to apply transformation to (opt,)'),
               'cost_function': ('eoptions.cost_fun',
                              'objective cost function (opt, nmi))'),
               'fwhm': ('eoptions.fwhm',
                        'gaussian smoothing kernel width (opt, 5)'),
               'separation': ('eoptions.sep',
                              'sampling separation in mm (opt, 4))'),
               'tolerance': ('eoptions.tol',
                             'acceptable tolerance for each of 12 params (opt,))'),
               'write_interp': ('roptions.interp',
                           'degree of b-spline used for interpolation (opt, 0)'),
               'write_wrap': ('roptions.wrap',
                        'Check if interpolation should wrap in [x,y,z] (opt, [0,0,0])'),
               'write_mask': ('roptions.mask',
                              'True/False mask output image (opt, False)')
               }

    def get_input_info(self):
        """ Provides information about inputs as a dict
            info = [Bunch(key=string,copy=bool,ext='.nii'),...]
        """
        info = [Bunch(key='target',copy=False),
                Bunch(key='source',copy=True),
                Bunch(key='apply_to_files',copy=True)]
        return info

    def _convert_inputs(self, opt, val):
        """Convert input to appropriate format for spm
        """
        if opt == 'target':
            return scans_for_fnames(filename_to_list(val),keep4d=True)
        if opt == 'source':
            return scans_for_fnames(filename_to_list(val),keep4d=True)
        if opt == 'apply_to_files':
            return scans_for_fnames(filename_to_list(val))
        if opt in ['write_wrap']:
            if len(val) != 3:
                raise ValueError('%s must have 3 elements'%opt)
        return val
    
    def _parse_inputs(self):
        """validate spm realign options if set to None ignore
        """
        einputs = super(Coregister, self)._parse_inputs(skip=('jobtype'))
        jobtype =  self.inputs.jobtype
        return [{'%s'%(jobtype):einputs[0]}]

    out_map = {'coregistered_source' : ('Coregistered source file',),
               'coregistered_files' : ('Coregistered other files',
                                       'apply_to_files')
               }
        
    def aggregate_outputs(self):
        if isinstance(self.inputs.source, list):
            source_ext = self.inputs.source[0][-4:]
        else:
            source_ext = self.inputs.source[-4:]         
        
        outputs = self.outputs()
        if self.inputs.jobtype == "estimate":
            if self.inputs.apply_to_files != None:
                outputs.coregistered_files = self.inputs.apply_to_files
            else:
                outputs.coregistered_files = self.inputs.source
        elif self.inputs.jobtype == "write":
            outputs.coregistered_files = []
            filelist = filename_to_list(self.inputs.source)
            for f in filelist:
                c_file = glob(fname_presuffix(f,prefix='r',suffix=source_ext,use_ext=False))
                assert len(c_file) == 1, 'No coregistered file generated by SPM Coregister %s'
                outputs.coregistered_files.append(c_file[0])
        else:
            c_source = glob(fname_presuffix(self.inputs.source,prefix='r',suffix=source_ext,use_ext=False))
            assert len(c_source) == 1, 'No coregistered files generated by SPM Coregister'
            outputs.coregistered_source = c_source[0]
            outputs.coregistered_files = []
            if self.inputs.apply_to_files:
                filelist = filename_to_list(self.inputs.apply_to_files)
                for f in filelist:
                    c_file = glob(fname_presuffix(f,prefix='r',suffix=source_ext,use_ext=False))
                    assert len(c_file) == 1, 'No coregistered file generated by SPM Coregister'
                    outputs.coregistered_files.append(c_file[0])
        outputs.coregistered_files = list_to_filename(outputs.coregistered_files)
        return outputs
        
    def run(self, target=None, source=None, **inputs):
        """Executes the SPM coregister function using MATLAB
        
        Parameters
        ----------
        
        target: string, list
            image file to coregister source to
        source: string, list
            image file that will be coregistered to the template 
        """
        if target:
            self.inputs.target = target
        if not self.inputs.target:
            raise AttributeError('Realign requires a target file')
        if source:
            self.inputs.source = source
        if not self.inputs.source:
            raise AttributeError('Realign requires a source file')
        
        self.inputs.update(**inputs)
        
        if self.inputs.jobtype == 'write':
            if self.inputs.apply_to_files != None:
                raise AttributeError('Reslice does not accept apply_to_files. Use source instead.')
                 
        return super(Coregister,self).run()


class Normalize(SpmMatlabCommandLine):
    """use spm_normalise for warping an image to a template

    Examples
    --------
    
    """
    
    def spm_doc(self):
        """Print out SPM documentation."""
        print grab_doc('Normalise: Estimate & Write')

    @property
    def cmd(self):
        return 'spm_normalise'

    @property
    def jobtype(self):
        return 'spatial'

    @property
    def jobname(self):
        return 'normalise'
    
    opt_map = {'template': ('eoptions.template', 'template file to normalize to'),
               'source': ('subj.source', 'file to normalize to template'),
               'jobtype': (None, 'one of: estimate, write, estwrite (opt, estwrite)', 'estwrite'),
               'apply_to_files': ('subj.resample',
                                  'files to apply transformation to (opt,)'),
               'parameter_file': ('subj.matname',
                                  'normalization parameter file*_sn.mat'),
               'source_weight': ('subj.wtsrc',
                                 'name of weighting image for source (opt)'),
               'template_weight': ('eoptions.weight',
                                   'name of weighting image for template (opt)'),
               'source_image_smoothing': ('eoptions.smosrc',
                                          'source smoothing (opt)'),
               'template_image_smoothing': ('eoptions.smoref',
                                            'template smoothing (opt)'),
               'affine_regularization_type': ('eoptions.regype',
                                              'mni, size, none (opt)'),
               'DCT_period_cutoff': ('eoptions.cutoff',
                                     'Cutoff of for DCT bases (opt, 25)'),
               'nonlinear_iterations': ('eoptions.nits',
                     'Number of iterations of nonlinear warping (opt, 16)'),
               'nonlinear_regularization': ('eoptions.reg',
                                            'min = 0; max = 1 (opt, 1)'),
               'write_preserve': ('roptions.preserve',
                     'True/False warped images are modulated (opt, False)'),
               'write_bounding_box': ('roptions.bb', '6-element list (opt,)'),
               'write_voxel_sizes': ('roptions.vox', '3-element list (opt,)'),
               'write_interp': ('roptions.interp',
                           'degree of b-spline used for interpolation (opt, 0)'),
               'write_wrap': ('roptions.wrap',
                        'Check if interpolation should wrap in [x,y,z] (opt, [0,0,0])'),
               }

    def get_input_info(self):
        """ Provides information about inputs as a dict
            info = [Bunch(key=string,copy=bool,ext='.nii'),...]
        """
        info = [Bunch(key='source',copy=False),
                Bunch(key='parameter_file',copy=False),
                Bunch(key='apply_to_files',copy=False)]
        return info
        
    def _convert_inputs(self, opt, val):
        """Convert input to appropriate format for spm
        """
        if opt == 'template':
            return scans_for_fname(filename_to_list(val))
        if opt == 'source':
            return scans_for_fname(filename_to_list(val))
        if opt == 'apply_to_files':
            return scans_for_fnames(filename_to_list(val))
        if opt == 'parameter_file':
            return np.array([list_to_filename(val)],dtype=object)
        if opt in ['write_wrap']:
            if len(val) != 3:
                raise ValueError('%s must have 3 elements'%opt)
        return val

    def _parse_inputs(self):
        """validate spm realign options if set to None ignore
        """
        einputs = super(Normalize, self)._parse_inputs(skip=('jobtype',
                                                             'apply_to_files'))
        if self.inputs.apply_to_files:
            inputfiles = deepcopy(filename_to_list(self.inputs.apply_to_files))
            if self.inputs.source:
                inputfiles.append(list_to_filename(self.inputs.source))
            einputs[0]['subj']['resample'] = scans_for_fnames(inputfiles)
        jobtype =  self.inputs.jobtype
        if jobtype in ['estwrite', 'write']:
            if self.inputs.apply_to_files is None:
                if self.inputs.source:
                    einputs[0]['subj']['resample'] = scans_for_fname(self.inputs.source)            
        return [{'%s'%(jobtype):einputs[0]}]

    def run(self, template=None, source=None, parameter_file=None, apply_to_files=None, **inputs):
        """Executes the SPM normalize function using MATLAB
        
        Parameters
        ----------
        
        template: string, list containing 1 filename
            template image file to normalize to
        source: source image file that is normalized
            to template.
        """
        if template:
            self.inputs.template = template
        if source:
            self.inputs.source = source
        if parameter_file:
            self.inputs.parameter_file = parameter_file
        if apply_to_files:
            self.inputs.apply_to_files = apply_to_files
            
        jobtype =  self.inputs.jobtype
        if jobtype.startswith('est'):
            if not self.inputs.template:
                raise AttributeError('Normalize estimation requires a target file')
            if not self.inputs.source:
                raise AttributeError('Realign requires a source file')
        else:
            if not self.inputs.apply_to_files:
                raise AttributeError('Normalize write requires a files to apply')
            if not self.inputs.parameter_file:
                raise AttributeError('Normalize write requires a transformation matrix')
            
        self.inputs.update(**inputs)
        return super(Normalize,self).run()
    

    out_map = {'normalization_parameters' : ('MAT file containing the normalization parameters',),
               'normalized_source' : ('Normalized source file',),
               'normalized_files' : ('Normalized other files',
                                     'apply_to_files')
               }
        
    def aggregate_outputs(self):
           
        outputs = self.outputs()
        jobtype =  self.inputs.jobtype
        if jobtype.startswith('est'):
            if isinstance(self.inputs.source, list):
                source_ext = self.inputs.source[0][-4:]
            else:
                source_ext = self.inputs.source[-4:]
                
            sourcefile = list_to_filename(self.inputs.source)
            n_param = glob(fname_presuffix(sourcefile,suffix='_sn.mat',use_ext=False))
            assert len(n_param) == 1, 'No normalization parameter files '\
                'generated by SPM Normalize'
            outputs.normalization_parameters = n_param
            n_source = glob(fname_presuffix(sourcefile,prefix='w',suffix=source_ext,use_ext=False))
            outputs.normalized_source = list_to_filename(n_source)
        outputs.normalized_files = []
        if self.inputs.apply_to_files is not None:
            if isinstance(self.inputs.apply_to_files, list):
                files_ext = self.inputs.apply_to_files[0][-4:]
            else:
                files_ext = self.inputs.apply_to_files[-4:]
                
            filelist = filename_to_list(self.inputs.apply_to_files)
            for f in filelist:
                n_file = glob(fname_presuffix(f,prefix='w',suffix=files_ext,use_ext=False))
                assert len(n_file) == 1, 'No normalized file %s generated by SPM Normalize'%n_file
                outputs.normalized_files.append(n_file[0])
        outputs.normalized_files = list_to_filename(outputs.normalized_files)
        return outputs
        
class Segment(SpmMatlabCommandLine):
    """use spm_segment to separate structural images into different
    tissue classes.

    Examples
    --------
    
    """
    
    def spm_doc(self):
        """Print out SPM documentation."""
        print grab_doc('Segment')
    
    @property
    def cmd(self):
        return 'spm_segment'

    @property
    def jobtype(self):
        return 'spatial'

    @property
    def jobname(self):
        return 'preproc'

    #Options to produce grey matter images: c1*.img, wc1*.img and
    #mwc1*.img. None: [0,0,0], Native Space: [0,0,1], Unmodulated Normalised:
    #[0,1,0], Modulated Normalised: [1,0,0], Native + Unmodulated Normalised:
    #[0,1,1], Native + Modulated Normalised: [1,0,1], Native + Modulated +
    #Unmodulated: [1,1,1], Modulated + Unmodulated Normalised: [1,1,0]
    
    opt_map = {'data': ('data', 'one scan per subject'),
               'gm_output_type': ('output.GM', '3-element list (opt,)'),
               'wm_output_type': ('output.WM', '3-element list (opt,)'),
               'csf_output_type': ('output.CSF', '3-element list (opt,)'),
               'save_bias_corrected': ('output.biascor',
                     'True/False produce a bias corrected image (opt, )'),
               'clean_masks': ('output.cleanup',
                     'clean using estimated brain mask 0(no)-2 (opt, )'),
               'tissue_prob_maps': ('opts.tpm',
                     'list of gray, white & csf prob. (opt,)'),
               'gaussians_per_class': ('opts.ngaus',
                     'num Gaussians capture intensity distribution (opt,)'),
               'affine_regularization': ('opts.regtype',
                      'mni, eastern, subj, none (opt,)'),
               'warping_regularization': ('opts.warpreg',
                      'Controls balance between parameters and data (opt, 1)'),
               'warp_frequency_cutoff': ('opts.warpco', 'Cutoff of DCT bases (opt,)'),
               'bias_regularization': ('opts.biasreg',
                      'no(0) - extremely heavy (10), (opt, )'),
               'bias_fwhm': ('opts.biasfwhm',
                      'FWHM of Gaussian smoothness of bias (opt,)'),
               'sampling_distance': ('opts.samp',
                      'Sampling distance on data for parameter estimation (opt,)'),
               'mask_image': ('opts.msk',
                      'Binary image to restrict parameter estimation (opt,)'),
               }

    def get_input_info(self):
        """ Provides information about inputs as a dict
            info = [Bunch(key=string,copy=bool,ext='.nii'),...]
        """
        info = [Bunch(key='data',copy=False)]
        return info
    
    def _convert_inputs(self, opt, val):
        """Convert input to appropriate format for spm
        """
        if opt in ['data', 'tissue_prob_maps']:
            if isinstance(val, list):
                return scans_for_fnames(val)
            else:
                return scans_for_fname(val)
        if opt == 'save_bias_corrected':
            return int(val)
        if opt == 'mask_image':
            return scans_for_fname(val)
        return val

    def run(self, data=None, **inputs):
        """Executes the SPM segment function using MATLAB
        
        Parameters
        ----------
        
        data: string, list
            image file to segment
        """
        if data:
            self.inputs.data = data
        if not self.inputs.data:
            raise AttributeError('Segment requires a data file')
        self.inputs.update(**inputs)
        return super(Segment,self).run()

    out_map = {'native_class_images' : ('native images for the 3 tissue types',),
               'normalized_class_images' : ('normalized images',),
               'modulated_class_images' : ('modulated, normalized images',),
               'native_gm_image' : ('native space grey probability map',),
               'normalized_gm_image' : ('normalized grey probability map',),
               'modulated_gm_image' : ('modulated, normalized grey probability map',),
               'native_wm_image' : ('native space white probability map',),
               'normalized_wm_image' : ('normalized white probability map',),
               'modulated_wm_image' : ('modulated, normalized white probability map',),
               'native_csf_image' : ('native space csf probability map',),
               'normalized_csf_image' : ('normalized csf probability map',),
               'modulated_csf_image' : ('modulated, normalized csf probability map'),
               'modulated_input_images' : ('modulated version of input image',),
               'transformation_mat' : ('Normalization transformation',),
               'inverse_transformation_mat' : ('Inverse normalization info',),
               }
        
    def aggregate_outputs(self):
        outputs = self.outputs()
        f = self.inputs.data
        m_file = glob(fname_presuffix(f,prefix='m',suffix='.nii',use_ext=False))
        outputs.modulated_input_image = m_file
        c_files = glob(fname_presuffix(f,prefix='c*',suffix='.nii',use_ext=False))
        outputs.native_class_images = c_files
        wc_files = glob(fname_presuffix(f,prefix='wc*',suffix='.nii',use_ext=False))
        outputs.normalized_class_images = wc_files
        mwc_files = glob(fname_presuffix(f,prefix='mwc*',suffix='.nii',use_ext=False))
        outputs.modulated_class_images = mwc_files
        
        c_files = glob(fname_presuffix(f,prefix='c1',suffix='.nii',use_ext=False))
        outputs.native_gm_image = c_files
        wc_files = glob(fname_presuffix(f,prefix='wc1',suffix='.nii',use_ext=False))
        outputs.normalized_gm_image = wc_files
        mwc_files = glob(fname_presuffix(f,prefix='mwc1',suffix='.nii',use_ext=False))
        outputs.modulated_gm_image = mwc_files
        <commit_msg>fixed a type in out_map<commit_after>"""The spm module provides basic functions for interfacing with matlab
and spm to access spm tools.

These functions include:
    
* Realign: within-modality registration

* Coregister: between modality registration
    
* Normalize: non-linear warping to standard space

* Segment: bias correction, segmentation

* Smooth: smooth with Gaussian kernel

"""
__docformat__ = 'restructuredtext'

# Standard library imports
import os
from glob import glob
from copy import deepcopy
import re

# Third-party imports
import numpy as np
from scipy.io import savemat

# Local imports
from nipype.interfaces.base import Bunch, InterfaceResult, Interface
from nipype.utils import setattr_on_read
from nipype.externals.pynifti import load
from nipype.interfaces.matlab import MatlabCommandLine
from nipype.utils.filemanip import (fname_presuffix, fnames_presuffix, 
                                    filename_to_list, list_to_filename,
                                    loadflat)
from nipype.utils.spm_docs import grab_doc
import logging
logger = logging.getLogger('spmlogger')

def scans_for_fname(fname):
    """Reads a nifti file and converts it to a numpy array storing
    individual nifti volumes
    
    Opens images so will fail if they are not found
    """
    if isinstance(fname,list):
        scans = np.zeros((len(fname),),dtype=object)
        for sno,f in enumerate(fname):
            scans[sno] = '%s,1'%f
        return scans
    img = load(fname)
    if len(img.get_shape()) == 3:
        return np.array(('%s,1'%fname,),dtype=object)
    else:
        n_scans = img.get_shape()[3]
        scans = np.zeros((n_scans,),dtype=object)
        for sno in range(n_scans):
            scans[sno] = '%s,%d'% (fname, sno+1)
        return scans

def scans_for_fnames(fnames,keep4d=False,separate_sessions=False):
    """Converts a list of files to a concatenated numpy array for each
    volume.

    keep4d : boolean
        keeps the entries of the numpy array as 4d files instead of
        extracting the individual volumes.
    separate_sessions: boolean
        if 4d nifti files are being used, then separate_sessions
        ensures a cell array per session is created in the structure.
    """
    flist = None
    if separate_sessions or keep4d:
        flist = np.zeros((len(fnames),),dtype=object)
    for i,f in enumerate(fnames):
        if separate_sessions:
            if keep4d:
                flist[i] = np.array([f],dtype=object)
            else:
                flist[i] = scans_for_fname(f)
        else:
            if keep4d:
                flist[i] = f
            else:
                scans = scans_for_fname(f)
                if flist is None:
                    flist = scans
                else:
                    flist = np.concatenate((flist,scans))
    return flist

class SpmInfo(object):
    """ Return the path to the spm directory in the matlab path
        If path not found, prints error asn returns None
    """
    __path = None
    @classmethod
    def spm_path(cls):
        if cls.__path == None:
            mlab = MatlabCommandLine()
            mlab.inputs.script_name = 'spminfo'
            mlab.inputs.script_lines = """
    if isempty(which('spm')), throw(MException('SPMCheck:NotFound','SPM not in matlab path'));end;
    spm_path = spm('dir');
    fprintf(1, '<PATH>%s</PATH>', spm_path);
    """
            mlab.inputs.mfile = False
            out = mlab.run()
            if out.runtime.returncode == 0:
                path = re.match('<PATH>(.*)</PATH>',out.runtime.stdout[out.runtime.stdout.find('<PATH>'):])
                if path is not None:
                    path = path.groups()[0]
                cls.__path = path
            else:
                print out.runtime.stderr
                return None
            
        return cls.__path

class SpmMatlabCommandLine(MatlabCommandLine):
    """ Extends the `MatlabCommandLine` class to handle SPM specific
    formatting of matlab scripts.
    """

    opt_map = {}

    def __init__(self, matlab_cmd=None, **inputs): 
        super(SpmMatlabCommandLine,self).__init__(**inputs)
        self.mfile = True

    @property
    def jobtype(self):
        """the jobtype used by spm/matlab
        to specify the jobtype to run
        jobs{1}.jobtype{1}.jobname"""
        return 'jobtype' 

    @property
    def jobname(self):
        """the jobname used by spm/matlab
        to specify the jobname to run
        jobs{1}.jobtype{1}.jobname"""
        return 'jobname' 

    def _use_mfile(self, use_mfile):
        """boolean,
        if true generates a matlab <filename>.m file
        if false generates a binary .mat file
        """
        self.mfile = use_mfile

    def run(self, **inputs):
        """Executes the SPM function using MATLAB
        """
        results = super(SpmMatlabCommandLine,self).run()
        if results.runtime.returncode == 0:
            results.outputs = self.aggregate_outputs() 
        return results

    def _populate_inputs(self):
        self.inputs = Bunch()
        for k,v in self.opt_map.items():
            if len(v)<3:
                setattr(self.inputs, k, None)
            else:
                setattr(self.inputs, k, v[2])
        #self.inputs = Bunch((k,None) for k in self.opt_map.keys())

    @classmethod
    def help(cls):
        cls.inputs_help()
        print ''
        cls.outputs_help()
        
    @classmethod
    def inputs_help(cls):
        helpstr = ['Inputs','------']
        opthelpstr = None
        manhelpstr = None
        for k,v in sorted(cls.opt_map.items()):
            if '(opt' in v[1]:
                if not opthelpstr:
                    opthelpstr = ['','Optional:']
                opthelpstr += ['%s: %s'%(k,v[1])]
            else:
                if not manhelpstr:
                    manhelpstr = ['','Mandatory:']
                manhelpstr += ['%s: %s'%(k,v[1])]
        if manhelpstr:
            helpstr += manhelpstr
        if opthelpstr:
            helpstr += opthelpstr
        print '\n'.join(helpstr)
        #return (helpstr,manhelpstr,opthelpstr)
        
    def _convert_inputs(self, opt, val):
        """Convert input to appropriate format for spm
        """
        return val
    
    def _parse_inputs(self, skip=()):
        spmdict = {}
        inputs = sorted((k, v) for k, v in self.inputs.items()
                            if v is not None and k not in skip)
        for opt, value in inputs:
            try:
                argstr = self.opt_map[opt][0]
                if '.' in argstr:
                    fields = argstr.split('.')
                    if fields[0] not in spmdict.keys():
                        spmdict[fields[0]] = {}
                    spmdict[fields[0]][fields[1]] = self._convert_inputs(opt,value)
                else:
                    spmdict[argstr] = self._convert_inputs(opt,value)
            except KeyError:
                logger.warn("Option '%s' is not supported!" % (opt))
                raise
        return [spmdict]

    @classmethod
    def outputs_help(cls):
        """ Prints the help of outputs
        """
        helpstr = ['Outputs','-------']
        for k,v in sorted(cls.out_map.items()):
            helpstr += ['%s: %s'%(k,v[0])]
        print '\n'.join(helpstr)

    def outputs(self):
        """
        """
        outputs = Bunch()
        for k in self.out_map.keys():
            setattr(outputs, k, None)
        return outputs
    
    def _compile_command(self):
        """Assembles the matlab code for SPM function
        
        Virtual function that needs to be implemented by the
        subclass
        """ 
        self._cmdline, mscript = self._make_matlab_command(deepcopy(self._parse_inputs()))
    
    def aggregate_outputs(self):
        """Collects all the outputs produced by an SPM function
        
        Virtual function that needs to be implemented by the
        subclass to collate outputs created generated by the SPM
        functionality being wrapped.
        """ 
        raise NotImplementedError
    
    def _reformat_dict_for_savemat(self, contents):
        """Encloses a dict representation within hierarchical lists.

        In order to create an appropriate SPM job structure, a Python
        dict storing the job needs to be modified so that each dict
        embedded in dict needs to be enclosed as a list element.

        Examples
        --------
        >>> a = SpmMatlabCommandLine()._reformat_dict_for_savemat(dict(a=1,b=dict(c=2,d=3)))
        >>> print a
        [{'a': 1, 'b': [{'c': 2, 'd': 3}]}]
        
        .. notes: XXX Need to talk to Matthew about cleaning up this code.
        """
        # XXX TODO Satra, I didn't change the semantics, but got rid of extraneous stuff.
        # This seems weird.  Please have a look and make sure you intend to
        # discard empty dicts. -DJC
        # if dict is empty, SPM falls back on defaults
        # ...so empty dicts are discarded -CM
        newdict = {}
        try:
            for key, value in contents.items():
                if isinstance(value, dict):
                    if value:
                        newdict[key] = self._reformat_dict_for_savemat(value)
                    # if value is None, skip
                else:
                    newdict[key] = value
                
            return [newdict]
        except TypeError:
            print 'Requires dict input'

    def _generate_job(self, prefix='', contents=None):
        """ Recursive function to generate spm job specification as a string

        Parameters
        ----------
        prefix : string
            A string that needs to get  
        contents : dict
            A non-tuple Python structure containing spm job
            information gets converted to an appropriate sequence of
            matlab commands.
        """
        jobstring = ''
        if contents is None:
            return jobstring
        if isinstance(contents, list):
            for i,value in enumerate(contents):
                newprefix = "%s(%d)" % (prefix, i+1)
                jobstring += self._generate_job(newprefix, value)
            return jobstring
        if isinstance(contents, dict):
            for key,value in contents.items():
                newprefix = "%s.%s" % (prefix, key)
                jobstring += self._generate_job(newprefix, value)
            return jobstring
        if isinstance(contents, np.ndarray):
            if contents.dtype == np.dtype(object):
                if prefix:
                    jobstring += "%s = {...\n"%(prefix)
                else:
                    jobstring += "{...\n"
                for i,val in enumerate(contents):
                    if isinstance(val, np.ndarray):
                        jobstring += self._generate_job(prefix=None,
                                                        contents=val)
                    elif isinstance(val,str):
                        jobstring += '\'%s\';...\n'%(val)
                    else:
                        jobstring += '%s;...\n'%str(val)
                jobstring += '};\n'
            else:
                for i,val in enumerate(contents):
                    for field in val.dtype.fields:
                        if prefix:
                            newprefix = "%s(%d).%s"%(prefix, i+1, field)
                        else:
                            newprefix = "(%d).%s"%(i+1, field)
                        jobstring += self._generate_job(newprefix,
                                                        val[field])
            return jobstring
        if isinstance(contents, str):
            jobstring += "%s = '%s';\n" % (prefix,contents)
            return jobstring
        jobstring += "%s = %s;\n" % (prefix,str(contents))
        return jobstring
    
    def _make_matlab_command(self, contents, cwd=None, postscript=None):
        """ generates a mfile to build job structure
        Parameters
        ----------

        contents : list
            a list of dicts generated by _parse_inputs
            in each subclass

        cwd : string
            default os.getcwd()

        Returns
        -------
        cmdline : string
            string representing command passed to shell
            as generated by matlab.MatlabCommandLine()._gen_matlab_command

        mscript : string
            contents of a script called by matlab
        """
        if not cwd:
            cwd = os.getcwd()
        mscript  = """
        %% Generated by nipype.interfaces.spm
        if isempty(which('spm')),
             throw(MException('SPMCheck:NotFound','SPM not in matlab path'));
        end
        fprintf('SPM version: %s\\n',spm('ver'));
        fprintf('SPM path: %s\\n',which('spm'));
        spm_defaults;
                  
        if strcmp(spm('ver'),'SPM8'), spm_jobman('initcfg');end\n
        """
        if self.mfile:
            if self.jobname in ['st','smooth','preproc','fmri_spec','fmri_est'] :
                mscript += self._generate_job('jobs{1}.%s{1}.%s(1)' % 
                                             (self.jobtype,self.jobname), contents[0])
            else:
                mscript += self._generate_job('jobs{1}.%s{1}.%s{1}' % 
                                             (self.jobtype,self.jobname), contents[0])
        else:
            jobdef = {'jobs':[{self.jobtype:[{self.jobname:self.reformat_dict_for_savemat
                                         (contents[0])}]}]}
            savemat(os.path.join(cwd,'pyjobs_%s.mat'%self.jobname), jobdef)
            mscript += "load pyjobs_%s;\n\n" % self.jobname
        mscript += """ 
        if strcmp(spm('ver'),'SPM8'), 
           jobs=spm_jobman('spm5tospm8',{jobs});
        end 
        spm_jobman(\'run\',jobs);\n
        """
        if postscript is not None:
            mscript += postscript
        cmdline = self._gen_matlab_command(mscript, cwd=cwd,
                                           script_name='pyscript_%s' % self.jobname,
                                           mfile=self.mfile) 
        return cmdline, mscript


class SliceTiming(SpmMatlabCommandLine):
    """Use spm to perform slice timing correction.

    See SliceTiming().spm_doc() for more information.

    Examples
    --------

    >>> from nipype.interfaces.spm import SliceTiming
    >>> st = SliceTiming()
    >>> st.inputs.infile = 'func.nii'
    >>> st.inputs.num_slices = 32
    >>> st.inputs.time_repetition = 6.0
    >>> st.inputs.time_acquisition = 6. - 6./32.
    >>> st.inputs.slice_order = range(32,0,-1)
    >>> st.inputs.ref_slice = 1
    """

    def spm_doc(self):
        """Print out SPM documentation."""
        print grab_doc('SliceTiming')
    
    @property
    def cmd(self):
        return 'spm_st'

    @property
    def jobtype(self):
        return 'temporal'

    @property
    def jobname(self):
        return 'st'

    opt_map = {'infile': ('scans',
                          'list of filenames to apply slice timing'),
               'num_slices': ('nslices',
                              'number of slices in a volume'),
               'time_repetition': ('tr',
                                   'time between volume acquisitions ' \
                                       '(start to start time)'),
               'time_acquisition': ('ta',
                                    'time of volume acquisition. usually ' \
                                        'calculated as TR-(TR/num_slices)'),
               'slice_order': ('so',
                               '1-based order in which slices are acquired'),
               'ref_slice': ('refslice',
                             '1-based Number of the reference slice')
               }
        
    def get_input_info(self):
        """ Provides information about inputs as a dict
            info = [Bunch(key=string,copy=bool,ext='.nii'),...]
        """
        info = [Bunch(key='infile',copy=False)]
        return info

    def _convert_inputs(self, opt, val):
        """Convert input to appropriate format for spm
        """
        if opt == 'infile':
            return scans_for_fnames(filename_to_list(val),
                                    separate_sessions=True)
        return val

    def run(self, infile=None, **inputs):
        """Executes the SPM slice timing function using MATLAB
        
        Parameters
        ----------
        
        infile: string, list
            image file(s) to smooth
        """
        if infile:
            self.inputs.infile = infile
        if not self.inputs.infile:
            raise AttributeError('Slice timing requires a file')
        self.inputs.update(**inputs)
        return super(SliceTiming,self).run()

    out_map = {'timecorrected_files' : ('slice time corrected files','infile')}
        
    def aggregate_outputs(self):
        outputs = self.outputs()
        outputs.timecorrected_files = []
        filelist = filename_to_list(self.inputs.infile)
        for f in filelist:
            s_file = glob(fname_presuffix(f, prefix='a',
                                          suffix='.nii',use_ext=False))
            assert len(s_file) == 1, 'No slice time corrected file generated by SPM Slice Timing'
            outputs.timecorrected_files.append(s_file[0])
        return outputs
    
class Realign(SpmMatlabCommandLine):
    """Use spm_realign for estimating within modality rigid body alignment

    Examples
    --------

    >>> import nipype.interfaces.spm as spm
    >>> realign = spm.Realign()
    >>> realign.inputs.infile = 'a.nii'
    >>> realign.inputs.register_to_mean = True
    >>> realign.run() # doctest: +SKIP

    """

    def spm_doc(self):
        """Print out SPM documentation."""
        print grab_doc('Realign: Estimate & Reslice')

    @property
    def cmd(self):
        return 'spm_realign'

    @property
    def jobtype(self):
         return 'spatial'

    @property
    def jobname(self):
        return 'realign'

    opt_map = {'infile': ('data', 'list of filenames to realign'),
               'jobtype': (None, 'one of: estimate, write, estwrite (opt, estwrite)', 'estwrite'),
               'quality': ('eoptions.quality',
                           '0.1 = fast, 1.0 = precise (opt, 0.9)'),
               'fwhm': ('eoptions.fwhm',
                        'gaussian smoothing kernel width (opt, 5)'),
               'separation': ('eoptions.sep',
                              'sampling separation in mm (opt, 4))'),
               'register_to_mean': ('eoptions.rtm', 'True/False (opt, False)'),
               'weight_img': ('eoptions.weight',
                              'filename of weighting image (opt, None)'),
               'interp': ('eoptions.interp',
                          'degree of b-spline used for interpolation (opt, 2)'),
               'wrap': ('eoptions.wrap',
                        'Check if interpolation should wrap in [x,y,z] (opt, [0,0,0])'),
               'write_which': ('roptions.which',
                               'determines which images to reslice (opt, [2, 1])'),
               'write_interp': ('roptions.interp',
                           'degree of b-spline used for interpolation (opt, 4)'),
               'write_wrap': ('roptions.wrap',
                        'Check if interpolation should wrap in [x,y,z] (opt, [0,0,0])'),
               'write_mask': ('roptions.mask', 'True/False mask output image (opt,)')
               }

    def get_input_info(self):
        """ Provides information about inputs
            info = [Bunch(key=string,copy=bool,ext='.nii'),...]
        """
        info = [Bunch(key='infile',copy=True)]
        return info

    def _convert_inputs(self, opt, val):
        """Convert input to appropriate format for spm
        """
        if opt == 'infile':
            return scans_for_fnames(filename_to_list(val),
                                    keep4d=True,
                                    separate_sessions=True)
        if opt == 'register_to_mean': # XX check if this is necessary
            return int(val)
        if opt in ['wrap','write_wrap']:
            if len(val) != 3:
                raise ValueError('%s must have 3 elements'%opt)
        if opt == 'write_which':
            if len(val) != 2:
                raise ValueError('write_which must have 2 elements')
        return val
    
    def _parse_inputs(self):
        """validate spm realign options if set to None ignore
        """
        einputs = super(Realign, self)._parse_inputs(skip=('jobtype'))
        jobtype =  self.inputs.jobtype
        return [{'%s'%(jobtype):einputs[0]}]

    out_map = {'realigned_files' : ('Realigned files',),
               'mean_image' : ('Mean image file from the realignment',),
               'realignment_parameters' : ('Estimated translation and rotation parameters',)
               }

    def run(self, infile=None,**inputs):
        """Executes the SPM realign function using MATLAB
        
        Parameters
        ----------
        
        infile: string, list
            list of filenames to realign
        """
        if infile:
            self.inputs.infile = infile
        if not self.inputs.infile:
            raise AttributeError('Realign requires an input file')
        self.inputs.update(**inputs)
        # The following line should call the run function of Realign's
        # parent class.
        return super(Realign,self).run()
    
    def aggregate_outputs(self):
        """ Initializes the output fields for this interface and then
        searches for and stores the data that go into those fields.
        """
        outputs = self.outputs()
        outputs.realigned_files = []
        outputs.realignment_parameters = []
        filelist = filename_to_list(self.inputs.infile)
        outputs.mean_image = glob(fname_presuffix(filelist[0],prefix='mean'))[0]
        for f in filelist:
            r_file = glob(fname_presuffix(f, prefix='r', suffix='.nii',
                                          use_ext=False))
            assert len(r_file) == 1, 'No realigned file generated by SPM Realign'
            outputs.realigned_files.append(r_file[0])
            rp_file = glob(fname_presuffix(f,prefix='rp_',suffix='.txt',use_ext=False))
            assert len(rp_file) == 1, 'No realignment parameter file generated by SPM Realign'
            outputs.realignment_parameters.append(rp_file[0])
        outputs.realigned_files = list_to_filename(outputs.realigned_files)
        return outputs

class Coregister(SpmMatlabCommandLine):
    """Use spm_coreg for estimating cross-modality rigid body alignment

    Examples
    --------
    
    >>> import nipype.interfaces.spm as spm
    >>> coreg = spm.Coregister()
    >>> coreg.inputs.infile = 'a.nii'
    >>> coreg.inputs.register_to_mean = True
    >>> coreg.run() # doctest: +SKIP
    
    """
    
    def spm_doc(self):
        """Print out SPM documentation."""
        print grab_doc('Coreg: Estimate & Reslice')

    @property
    def cmd(self):
        return 'spm_coreg'

    @property
    def jobtype(self):
        return 'spatial'

    @property
    def jobname(self):
        return 'coreg'
        
    opt_map = {'target': ('ref', 'reference file to register to'),
               'source': ('source', 'file to register to target'),
               'jobtype': (None, 'one of: estimate, write, estwrite (opt,estwrite)','estwrite'),
               'apply_to_files': ('other', 'files to apply transformation to (opt,)'),
               'cost_function': ('eoptions.cost_fun',
                              'objective cost function (opt, nmi))'),
               'fwhm': ('eoptions.fwhm',
                        'gaussian smoothing kernel width (opt, 5)'),
               'separation': ('eoptions.sep',
                              'sampling separation in mm (opt, 4))'),
               'tolerance': ('eoptions.tol',
                             'acceptable tolerance for each of 12 params (opt,))'),
               'write_interp': ('roptions.interp',
                           'degree of b-spline used for interpolation (opt, 0)'),
               'write_wrap': ('roptions.wrap',
                        'Check if interpolation should wrap in [x,y,z] (opt, [0,0,0])'),
               'write_mask': ('roptions.mask',
                              'True/False mask output image (opt, False)')
               }

    def get_input_info(self):
        """ Provides information about inputs as a dict
            info = [Bunch(key=string,copy=bool,ext='.nii'),...]
        """
        info = [Bunch(key='target',copy=False),
                Bunch(key='source',copy=True),
                Bunch(key='apply_to_files',copy=True)]
        return info

    def _convert_inputs(self, opt, val):
        """Convert input to appropriate format for spm
        """
        if opt == 'target':
            return scans_for_fnames(filename_to_list(val),keep4d=True)
        if opt == 'source':
            return scans_for_fnames(filename_to_list(val),keep4d=True)
        if opt == 'apply_to_files':
            return scans_for_fnames(filename_to_list(val))
        if opt in ['write_wrap']:
            if len(val) != 3:
                raise ValueError('%s must have 3 elements'%opt)
        return val
    
    def _parse_inputs(self):
        """validate spm realign options if set to None ignore
        """
        einputs = super(Coregister, self)._parse_inputs(skip=('jobtype'))
        jobtype =  self.inputs.jobtype
        return [{'%s'%(jobtype):einputs[0]}]

    out_map = {'coregistered_source' : ('Coregistered source file',),
               'coregistered_files' : ('Coregistered other files',
                                       'apply_to_files')
               }
        
    def aggregate_outputs(self):
        if isinstance(self.inputs.source, list):
            source_ext = self.inputs.source[0][-4:]
        else:
            source_ext = self.inputs.source[-4:]         
        
        outputs = self.outputs()
        if self.inputs.jobtype == "estimate":
            if self.inputs.apply_to_files != None:
                outputs.coregistered_files = self.inputs.apply_to_files
            else:
                outputs.coregistered_files = self.inputs.source
        elif self.inputs.jobtype == "write":
            outputs.coregistered_files = []
            filelist = filename_to_list(self.inputs.source)
            for f in filelist:
                c_file = glob(fname_presuffix(f,prefix='r',suffix=source_ext,use_ext=False))
                assert len(c_file) == 1, 'No coregistered file generated by SPM Coregister %s'
                outputs.coregistered_files.append(c_file[0])
        else:
            c_source = glob(fname_presuffix(self.inputs.source,prefix='r',suffix=source_ext,use_ext=False))
            assert len(c_source) == 1, 'No coregistered files generated by SPM Coregister'
            outputs.coregistered_source = c_source[0]
            outputs.coregistered_files = []
            if self.inputs.apply_to_files:
                filelist = filename_to_list(self.inputs.apply_to_files)
                for f in filelist:
                    c_file = glob(fname_presuffix(f,prefix='r',suffix=source_ext,use_ext=False))
                    assert len(c_file) == 1, 'No coregistered file generated by SPM Coregister'
                    outputs.coregistered_files.append(c_file[0])
        outputs.coregistered_files = list_to_filename(outputs.coregistered_files)
        return outputs
        
    def run(self, target=None, source=None, **inputs):
        """Executes the SPM coregister function using MATLAB
        
        Parameters
        ----------
        
        target: string, list
            image file to coregister source to
        source: string, list
            image file that will be coregistered to the template 
        """
        if target:
            self.inputs.target = target
        if not self.inputs.target:
            raise AttributeError('Realign requires a target file')
        if source:
            self.inputs.source = source
        if not self.inputs.source:
            raise AttributeError('Realign requires a source file')
        
        self.inputs.update(**inputs)
        
        if self.inputs.jobtype == 'write':
            if self.inputs.apply_to_files != None:
                raise AttributeError('Reslice does not accept apply_to_files. Use source instead.')
                 
        return super(Coregister,self).run()


class Normalize(SpmMatlabCommandLine):
    """use spm_normalise for warping an image to a template

    Examples
    --------
    
    """
    
    def spm_doc(self):
        """Print out SPM documentation."""
        print grab_doc('Normalise: Estimate & Write')

    @property
    def cmd(self):
        return 'spm_normalise'

    @property
    def jobtype(self):
        return 'spatial'

    @property
    def jobname(self):
        return 'normalise'
    
    opt_map = {'template': ('eoptions.template', 'template file to normalize to'),
               'source': ('subj.source', 'file to normalize to template'),
               'jobtype': (None, 'one of: estimate, write, estwrite (opt, estwrite)', 'estwrite'),
               'apply_to_files': ('subj.resample',
                                  'files to apply transformation to (opt,)'),
               'parameter_file': ('subj.matname',
                                  'normalization parameter file*_sn.mat'),
               'source_weight': ('subj.wtsrc',
                                 'name of weighting image for source (opt)'),
               'template_weight': ('eoptions.weight',
                                   'name of weighting image for template (opt)'),
               'source_image_smoothing': ('eoptions.smosrc',
                                          'source smoothing (opt)'),
               'template_image_smoothing': ('eoptions.smoref',
                                            'template smoothing (opt)'),
               'affine_regularization_type': ('eoptions.regype',
                                              'mni, size, none (opt)'),
               'DCT_period_cutoff': ('eoptions.cutoff',
                                     'Cutoff of for DCT bases (opt, 25)'),
               'nonlinear_iterations': ('eoptions.nits',
                     'Number of iterations of nonlinear warping (opt, 16)'),
               'nonlinear_regularization': ('eoptions.reg',
                                            'min = 0; max = 1 (opt, 1)'),
               'write_preserve': ('roptions.preserve',
                     'True/False warped images are modulated (opt, False)'),
               'write_bounding_box': ('roptions.bb', '6-element list (opt,)'),
               'write_voxel_sizes': ('roptions.vox', '3-element list (opt,)'),
               'write_interp': ('roptions.interp',
                           'degree of b-spline used for interpolation (opt, 0)'),
               'write_wrap': ('roptions.wrap',
                        'Check if interpolation should wrap in [x,y,z] (opt, [0,0,0])'),
               }

    def get_input_info(self):
        """ Provides information about inputs as a dict
            info = [Bunch(key=string,copy=bool,ext='.nii'),...]
        """
        info = [Bunch(key='source',copy=False),
                Bunch(key='parameter_file',copy=False),
                Bunch(key='apply_to_files',copy=False)]
        return info
        
    def _convert_inputs(self, opt, val):
        """Convert input to appropriate format for spm
        """
        if opt == 'template':
            return scans_for_fname(filename_to_list(val))
        if opt == 'source':
            return scans_for_fname(filename_to_list(val))
        if opt == 'apply_to_files':
            return scans_for_fnames(filename_to_list(val))
        if opt == 'parameter_file':
            return np.array([list_to_filename(val)],dtype=object)
        if opt in ['write_wrap']:
            if len(val) != 3:
                raise ValueError('%s must have 3 elements'%opt)
        return val

    def _parse_inputs(self):
        """validate spm realign options if set to None ignore
        """
        einputs = super(Normalize, self)._parse_inputs(skip=('jobtype',
                                                             'apply_to_files'))
        if self.inputs.apply_to_files:
            inputfiles = deepcopy(filename_to_list(self.inputs.apply_to_files))
            if self.inputs.source:
                inputfiles.append(list_to_filename(self.inputs.source))
            einputs[0]['subj']['resample'] = scans_for_fnames(inputfiles)
        jobtype =  self.inputs.jobtype
        if jobtype in ['estwrite', 'write']:
            if self.inputs.apply_to_files is None:
                if self.inputs.source:
                    einputs[0]['subj']['resample'] = scans_for_fname(self.inputs.source)            
        return [{'%s'%(jobtype):einputs[0]}]

    def run(self, template=None, source=None, parameter_file=None, apply_to_files=None, **inputs):
        """Executes the SPM normalize function using MATLAB
        
        Parameters
        ----------
        
        template: string, list containing 1 filename
            template image file to normalize to
        source: source image file that is normalized
            to template.
        """
        if template:
            self.inputs.template = template
        if source:
            self.inputs.source = source
        if parameter_file:
            self.inputs.parameter_file = parameter_file
        if apply_to_files:
            self.inputs.apply_to_files = apply_to_files
            
        jobtype =  self.inputs.jobtype
        if jobtype.startswith('est'):
            if not self.inputs.template:
                raise AttributeError('Normalize estimation requires a target file')
            if not self.inputs.source:
                raise AttributeError('Realign requires a source file')
        else:
            if not self.inputs.apply_to_files:
                raise AttributeError('Normalize write requires a files to apply')
            if not self.inputs.parameter_file:
                raise AttributeError('Normalize write requires a transformation matrix')
            
        self.inputs.update(**inputs)
        return super(Normalize,self).run()
    

    out_map = {'normalization_parameters' : ('MAT file containing the normalization parameters',),
               'normalized_source' : ('Normalized source file',),
               'normalized_files' : ('Normalized other files',
                                     'apply_to_files')
               }
        
    def aggregate_outputs(self):
           
        outputs = self.outputs()
        jobtype =  self.inputs.jobtype
        if jobtype.startswith('est'):
            if isinstance(self.inputs.source, list):
                source_ext = self.inputs.source[0][-4:]
            else:
                source_ext = self.inputs.source[-4:]
                
            sourcefile = list_to_filename(self.inputs.source)
            n_param = glob(fname_presuffix(sourcefile,suffix='_sn.mat',use_ext=False))
            assert len(n_param) == 1, 'No normalization parameter files '\
                'generated by SPM Normalize'
            outputs.normalization_parameters = n_param
            n_source = glob(fname_presuffix(sourcefile,prefix='w',suffix=source_ext,use_ext=False))
            outputs.normalized_source = list_to_filename(n_source)
        outputs.normalized_files = []
        if self.inputs.apply_to_files is not None:
            if isinstance(self.inputs.apply_to_files, list):
                files_ext = self.inputs.apply_to_files[0][-4:]
            else:
                files_ext = self.inputs.apply_to_files[-4:]
                
            filelist = filename_to_list(self.inputs.apply_to_files)
            for f in filelist:
                n_file = glob(fname_presuffix(f,prefix='w',suffix=files_ext,use_ext=False))
                assert len(n_file) == 1, 'No normalized file %s generated by SPM Normalize'%n_file
                outputs.normalized_files.append(n_file[0])
        outputs.normalized_files = list_to_filename(outputs.normalized_files)
        return outputs
        
class Segment(SpmMatlabCommandLine):
    """use spm_segment to separate structural images into different
    tissue classes.

    Examples
    --------
    
    """
    
    def spm_doc(self):
        """Print out SPM documentation."""
        print grab_doc('Segment')
    
    @property
    def cmd(self):
        return 'spm_segment'

    @property
    def jobtype(self):
        return 'spatial'

    @property
    def jobname(self):
        return 'preproc'

    #Options to produce grey matter images: c1*.img, wc1*.img and
    #mwc1*.img. None: [0,0,0], Native Space: [0,0,1], Unmodulated Normalised:
    #[0,1,0], Modulated Normalised: [1,0,0], Native + Unmodulated Normalised:
    #[0,1,1], Native + Modulated Normalised: [1,0,1], Native + Modulated +
    #Unmodulated: [1,1,1], Modulated + Unmodulated Normalised: [1,1,0]
    
    opt_map = {'data': ('data', 'one scan per subject'),
               'gm_output_type': ('output.GM', '3-element list (opt,)'),
               'wm_output_type': ('output.WM', '3-element list (opt,)'),
               'csf_output_type': ('output.CSF', '3-element list (opt,)'),
               'save_bias_corrected': ('output.biascor',
                     'True/False produce a bias corrected image (opt, )'),
               'clean_masks': ('output.cleanup',
                     'clean using estimated brain mask 0(no)-2 (opt, )'),
               'tissue_prob_maps': ('opts.tpm',
                     'list of gray, white & csf prob. (opt,)'),
               'gaussians_per_class': ('opts.ngaus',
                     'num Gaussians capture intensity distribution (opt,)'),
               'affine_regularization': ('opts.regtype',
                      'mni, eastern, subj, none (opt,)'),
               'warping_regularization': ('opts.warpreg',
                      'Controls balance between parameters and data (opt, 1)'),
               'warp_frequency_cutoff': ('opts.warpco', 'Cutoff of DCT bases (opt,)'),
               'bias_regularization': ('opts.biasreg',
                      'no(0) - extremely heavy (10), (opt, )'),
               'bias_fwhm': ('opts.biasfwhm',
                      'FWHM of Gaussian smoothness of bias (opt,)'),
               'sampling_distance': ('opts.samp',
                      'Sampling distance on data for parameter estimation (opt,)'),
               'mask_image': ('opts.msk',
                      'Binary image to restrict parameter estimation (opt,)'),
               }

    def get_input_info(self):
        """ Provides information about inputs as a dict
            info = [Bunch(key=string,copy=bool,ext='.nii'),...]
        """
        info = [Bunch(key='data',copy=False)]
        return info
    
    def _convert_inputs(self, opt, val):
        """Convert input to appropriate format for spm
        """
        if opt in ['data', 'tissue_prob_maps']:
            if isinstance(val, list):
                return scans_for_fnames(val)
            else:
                return scans_for_fname(val)
        if opt == 'save_bias_corrected':
            return int(val)
        if opt == 'mask_image':
            return scans_for_fname(val)
        return val

    def run(self, data=None, **inputs):
        """Executes the SPM segment function using MATLAB
        
        Parameters
        ----------
        
        data: string, list
            image file to segment
        """
        if data:
            self.inputs.data = data
        if not self.inputs.data:
            raise AttributeError('Segment requires a data file')
        self.inputs.update(**inputs)
        return super(Segment,self).run()

    out_map = {'native_class_images' : ('native images for the 3 tissue types',),
               'normalized_class_images' : ('normalized images',),
               'modulated_class_images' : ('modulated, normalized images',),
               'native_gm_image' : ('native space grey probability map',),
               'normalized_gm_image' : ('normalized grey probability map',),
               'modulated_gm_image' : ('modulated, normalized grey probability map',),
               'native_wm_image' : ('native space white probability map',),
               'normalized_wm_image' : ('normalized white probability map',),
               'modulated_wm_image' : ('modulated, normalized white probability map',),
               'native_csf_image' : ('native space csf probability map',),
               'normalized_csf_image' : ('normalized csf probability map',),
               'modulated_csf_image' : ('modulated, normalized csf probability map'),
               'modulated_input_image' : ('modulated version of input image',),
               'transformation_mat' : ('Normalization transformation',),
               'inverse_transformation_mat' : ('Inverse normalization info',),
               }
        
    def aggregate_outputs(self):
        outputs = self.outputs()
        f = self.inputs.data
        m_file = glob(fname_presuffix(f,prefix='m',suffix='.nii',use_ext=False))
        outputs.modulated_input_image = m_file
        c_files = glob(fname_presuffix(f,prefix='c*',suffix='.nii',use_ext=False))
        outputs.native_class_images = c_files
        wc_files = glob(fname_presuffix(f,prefix='wc*',suffix='.nii',use_ext=False))
        outputs.normalized_class_images = wc_files
        mwc_files = glob(fname_presuffix(f,prefix='mwc*',suffix='.nii',use_ext=False))
        outputs.modulated_class_images = mwc_files
        
        c_files = glob(fname_presuffix(f,prefix='c1',suffix='.nii',use_ext=False))
        outputs.native_gm_image = c_files
        wc_files = glob(fname_presuffix(f,prefix='wc1',suffix='.nii',use_ext=False))
        outputs.normalized_gm_image = wc_files
        mwc_files = glob(fname_presuffix(f,prefix='mwc1',suffix='.nii',use_ext=False))
        outputs.modulated_gm_image = mwc_files
        
{'fraction_non_alphanumeric': 0.11368890258793404, 'fraction_numerical': 0.004504878309010942, 'mean_word_length': 1.8194124135164946, 'pattern_counts': {'":': 4, '<': 17, '<?xml version=': 0, '>': 135, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 88, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '6853619', 'n_tokens_mistral': 24095, 'n_tokens_neox': 23176, 'n_words': 5878}