diff --git a/fsl/data/featdesign.py b/fsl/data/featdesign.py new file mode 100644 index 0000000000000000000000000000000000000000..c9b051b7c8e55e1acb33ff465126e8618d70e920 --- /dev/null +++ b/fsl/data/featdesign.py @@ -0,0 +1,332 @@ +#!/usr/bin/env python +# +# featdesign.py - +# +# Author: Paul McCarthy <pauldmccarthy@gmail.com> +# +"""This module provides the :class:`FEATFSFDesign` class, which encapsulates +a FEAT design matrix. + + +The FEAT design matrix +---------------------- + + +A FEAT design matrix may contain the following types of explanatory variables: + + - *Normal* EVs. This is simply a column in the design matrix, defined by the + user. + + - Temporal derivative of normal EVs. A column in the design matrix containing + the derivative of the previous normal EV. The presence of a temporal + derivative EV for a given normal EV can be determined by the ``deriv_yn`` + flag in the ``design.fsf`` file. + + - Basis function EV. One or more columns derived from a normal EV + (``basisfnumN``) + + - Voxelwise EVs (``designVoxelwiseEV<N>.nii.gz``, EV number must be offset by + temporal derivative EVs) + + - Confound EVs (... ?) + + - Voxelwise confound EVs (``vef.dat`` and ``ven.dat``) + + - Motion parameter EVs (``design.fsf:motionevs``, the last 6 or 24 columns of + the design matrix [i think]) + +For each voxelwise EV, the design matrix (in ``design.mat``) contains a +'dummy' column which contains the mean across all voxels. + + +For voxelwise EVs, the column number (1-indexed) is conatined in the file name +(``<N>`` in the above list entry). But this number does not take into account +the temporal derivative EVs of regular evs, so you need to offset this number +by the number of TD EVs in the design matrix, which come before the voxelwise +EV. + + +For voxelwise confound EVs, the column number mappings (1-indexed) are +contained in ``vef.dat`` and ``ven.dat``. + + +*Original* EV: An EV defined by the user +*Real* EV: A derived EV (either temporal derivative, or derived with + basis functions). +""" + + +import logging +import collections +import os.path as op +import numpy as np + +from . import featanalysis +from . import image as fslimage + + +log = logging.getLogger(__name__) + + +class FSFError(Exception): + pass + + +class EV(object): + def __init__(self, index, title): + self.index = index + self.title = title + + +class NormalEV(EV): + def __init__(self, realIdx, origIdx, title): + EV.__init__(self, realIdx, title) + self.origIndex = origIdx + + +class TemporalDerivativeEV(NormalEV): + pass + + +class BasisFunctionEV(NormalEV): + pass + + +class VoxelwiseEV(NormalEV): + def __init__(self, realIdx, origIdx, title, filename): + NormalEV.__init__(self, realIdx, origIdx, title) + self.filename = filename + + +class ConfoundEV(EV): + def __init__(self, index, confIndex, title): + EV.__init__(self, index, title) + self.confIndex = confIndex + + +class MotionParameterEV(EV): + def __init__(self, index, motionIndex, title): + EV.__init__(self, index, title) + self.motionIndex = motionIndex + + +class VoxelwiseConfoundEV(EV): + def __init__(self, index, voxIndex, title, filename): + EV.__init__(self, index, title) + self.voxIndex = voxIndex + self.filename = filename + + +class FEATFSFDesign(object): + """ + """ + + + def __init__(self, featDir, settings, designMatrix): + + # Get some information about the analysis + version = float(settings['version']) + level = int( settings['level']) + + # Print a warning if we're + # using an old version of FEAT + if version < 6: + log.warning('Unsupported FEAT version: {}'.format(version)) + + # We need to parse the EVS a bit + # differently depending on whether + # this is a first level or higher + # level analysis. + if level == 1: getEVs = getFirstLevelEVs + else: getEVs = getHigherLevelEVs + + self.__settings = collections.OrderedDict(settings.items()) + self.__design = np.array(designMatrix) + self.__numEVs = self.__design.shape[1] + self.__evs = getEVs(featDir, self.__settings, self.__design) + + for i, ev in enumerate(self.__evs): + + print 'EV{}: {} [{}]'.format( + ev.index + 1, + ev.title, + type(ev).__name__) + + if len(self.__evs) != self.__numEVs: + raise FSFError('Number of EVs does not match design.mat') + + + + def getDesign(self, x, y, z): + """Returns the design matrix for the specified voxel. + """ + + # if no vox EVs, just + # return the design + pass + + + def getVoxelEVFile(self, idx): + return self.__evs[idx].filename + + + def getVoxelConfoundFile(self, idx): + return self.__evs[idx].filename + + + +def getFirstLevelEVs(featDir, settings, designMat): + + evs = [] + origEVs = int(settings['evs_orig']) + + # First, we loop through the EVs that + # are explicitly defined in design.fsf. + # This includes + # - normal EVs + # - temporal derivative EVs + # - basis function EVs + # - voxelwise EVs + for origIdx in range(origEVs): + + title = settings[ 'evtitle{}' .format(origIdx + 1)] + shape = int(settings[ 'shape{}' .format(origIdx + 1)]) + convolve = int(settings[ 'convolve{}' .format(origIdx + 1)]) + deriv = int(settings[ 'deriv_yn{}' .format(origIdx + 1)]) + basis = int(settings.get('basisfnum{}'.format(origIdx + 1), -1)) + + # Normal EV. This is just a column + # in the design matrix, defined by + # the user. + if shape != 9: + evs.append(NormalEV(len(evs), origIdx, title)) + + # Voxelwise EV. This is a 'normal' EV + # defined by the user, with different + # values for each voxel. The voxelwise + # values should be contained in the + # feat directory, in an image called + # designVoxelwiseEVN, where N is the + # original EV index. + else: + + # The addExt function will + # raise an error if the + # file does not exist. + filename = op.join( + featDir, 'designVoxelwiseEV{}'.format(origIdx + 1)) + filename = fslimage.addExt(filename, True) + + evs.append(VoxelwiseEV(len(evs), origIdx, title, filename)) + + # This EV has been convolved with a set of basis + # functions. A set of N additional EVs have been + # added to the design matrix, immediately after + # the EV, where N is specified by the basisfnumN + # parameter in design.fsf. + if convolve in (4, 5, 6): + + if basis == -1: + raise FSFError('Number of EVs is not specified ' + 'for basis function EV') + + for i in range(basis - 1): + evs.append(BasisFunctionEV(len(evs), origIdx, title)) + + # A temporal derivative EV has been + # added for this EV - in the design + # matrix, it is the column immediately + # after this EV. + if deriv == 1: + evs.append(TemporalDerivativeEV(len(evs), origIdx, title)) + + # In the design matrix, after all EVs which + # have been explicilty defined, the rest of + # the EVs in the design matrix are confounds, + # in the following order: + # + # 1. Voxelwise confounds + # 2. Motion parameters + # 3. Other confounds + + # Any voxelwise confounds are specified + # in two plain text files - vef.dat + # contains a comma-separated list of + # files, and ven.dat contains the column + # index of this confound in the design + # matrix (1-indexed). If these files + # don't exist, then it means that there + # are no voxelwise confounds. + # + # n.b. Even though the indices into the + # final design matrix are stored in ven.dat, + # I'm just assuming that the voxelwise + # confound columns are immediately after + # the 'real' EVs procesed above, in the + # order defined in vef.dat. + voxConfFiles = op.join(featDir, 'vef.dat') + voxConfLocs = op.join(featDir, 'ven.dat') + + if op.exists(voxConfFiles) and op.exists(voxConfLocs): + + with open(voxConfFiles, 'rt') as vcff: + voxConfFiles = vcff.read() + + with open(voxConfLocs, 'rt') as vclf: + voxConfLocs = vclf.read() + + voxConfFiles = voxConfFiles.strip().split(',') + voxConfLocs = voxConfLocs .strip().split(',') + + if len(voxConfFiles) != len(voxConfLocs): + raise FSFError('vef.dat does not match ven.dat') + + # An error will be raised if any of + # the files in vef.dat do not exist, + # or if any of the indices in + # ven.dat are not integers. + voxConfFiles = [op.join(featDir, f) for f in voxConfFiles] + voxConfFiles = [fslimage.addExt(f, True) for f in voxConfFiles] + voxConfLocs = [int(i) for i in voxConfLocs] + + # Check to see if my assumption + # above, about the voxelwise + # confound EV locations, holds + startIdx = len(evs) + 1 + if voxConfLocs != range(startIdx, startIdx + len(voxConfFiles)): + raise FSFError('Unsupported voxelwise confound ordering ' + '({} -> {})'.format(startIdx, voxConfLocs)) + + # Create the voxelwise confound EVs + for i, (f, l) in enumerate(zip(voxConfFiles, voxConfLocs)): + evs.append(VoxelwiseConfoundEV(len(evs), i, 'voxconf', f)) + + # Have motion parameters been added + # as regressors to the design matrix? + motion = int(settings['motionevs']) + + if motion == 0: numMotionEVs = 0 + elif motion == 1: numMotionEVs = 6 + elif motion == 2: numMotionEVs = 24 + + for i in range(numMotionEVs): + evs.append(MotionParameterEV(len(evs), i, 'motion')) + + # Last step - any columns in the design + # matrix which have not yet been accounted + # for are other confounds, specified by + # the user with a text file. + numConfoundEVs = designMat.shape[1] - len(evs) + for i in range(numConfoundEVs): + evs.append(ConfoundEV(len(evs), i, 'confound')) + + # Phew. + return evs + + +def getHigherLevelEVs(featDir, settings, designMat): + + titleKeys = [s for s in settings.keys() if s.startswith('evtitle')] + evs = [] + + return evs