From bb6e10a8096fe40a275a7776b2a4f63ba13089a1 Mon Sep 17 00:00:00 2001 From: Paul McCarthy <pauld.mccarthy@gmail.com> Date: Fri, 4 Nov 2016 14:13:48 +0000 Subject: [PATCH] Further tweaks to FEAT model fit - trying to get visually sensible scaling on partial model fits. Also added new method isFirstLevelAnalysis. --- fsl/data/featanalysis.py | 10 ++++++ fsl/data/featimage.py | 70 +++++++++++++++++++++++++-------------- fsl/data/melodiclabels.py | 3 +- 3 files changed, 58 insertions(+), 25 deletions(-) diff --git a/fsl/data/featanalysis.py b/fsl/data/featanalysis.py index 63cbcc9d0..8aa24c4a7 100644 --- a/fsl/data/featanalysis.py +++ b/fsl/data/featanalysis.py @@ -19,6 +19,7 @@ following functions are provided: hasMelodicDir getAnalysisDir getTopLevelAnalysisDir + isFirstLevelAnalysis loadDesign loadContrasts loadSettings @@ -279,6 +280,15 @@ def getThresholds(settings): } +def isFirstLevelAnalysis(settings): + """Returns ``True`` if the FEAT analysis described by ``settings`` + is a first level analysis, ``False`` otherwise. + + :arg settings: A FEAT settings dictionary (see :func:`loadSettings`). + """ + return settings['level'] == '1' + + def loadClusterResults(featdir, settings, contrast): """If cluster thresholding was used in the FEAT analysis, this function will load and return the cluster results for the specified (0-indexed) diff --git a/fsl/data/featimage.py b/fsl/data/featimage.py index d9fa82cf4..d5857d50a 100644 --- a/fsl/data/featimage.py +++ b/fsl/data/featimage.py @@ -45,7 +45,7 @@ class FEATImage(fslimage.Image): # [23, 30, 42] (in this example, we # have 4 EVs - the first argument # is a contrast vector). - img.fit([1, 1, 1, 1], [23, 30, 42], fullmodel=True) + img.fit([1, 1, 1, 1], [23, 30, 42], fullModel=True) """ @@ -111,6 +111,13 @@ class FEATImage(fslimage.Image): return self.__analysisName + def isFirstLevelAnalysis(self): + """Returns ``True`` if the FEAT analysis described by ``settings`` + is a first level analysis, ``False`` otherwise. + """ + return featanalysis.isFirstLevelAnalysis(self.__settings) + + def getTopLevelAnalysisDir(self): """Returns the path to the higher level analysis directory of which this FEAT analysis is a part, or ``None`` if this analysis @@ -287,12 +294,12 @@ class FEATImage(fslimage.Image): return self.__clustMasks[con] - def fit(self, contrast, xyz, fullmodel=False): + def fit(self, contrast, xyz, fullModel=False): """Calculates the model fit for the given contrast vector at the given voxel. - Passing in a contrast of all 1s, and ``fullmodel=True`` will - get you the full model fit. Pass in ``fullmodel=False`` for + Passing in a contrast of all 1s, and ``fullModel=True`` will + get you the full model fit. Pass in ``fullModel=False`` for all other contrasts, otherwise the model fit values will not be scaled correctly. @@ -302,13 +309,20 @@ class FEATImage(fslimage.Image): :arg xyz: Coordinates of the voxel to calculate the model fit for. - :arg fullmodel: Set to ``True`` for a full model fit, ``False`` + :arg fullModel: Set to ``True`` for a full model fit, ``False`` otherwise. """ if self.__design is None: raise RuntimeError('No design') + x, y, z = xyz + firstLevel = self.isFirstLevelAnalysis() + numEVs = self.numEVs() + + if len(contrast) != numEVs: + raise ValueError('Contrast is wrong length') + # Here we are basically trying to # replicate the behaviour of tsplot. # There are some differences though - @@ -316,19 +330,21 @@ class FEATImage(fslimage.Image): # data by Z statistics. We're not # doing that here. - # If we are not plotting a full model fit, - # normalise the contrast vector to unit - # length. Why? Because that's what tsplot - # does. - if not fullmodel: - contrast = np.array(contrast) - contrast = contrast / np.sqrt((contrast ** 2).sum()) - - x, y, z = xyz - numEVs = self.numEVs() - - if len(contrast) != numEVs: - raise ValueError('Contrast is wrong length') + # Normalise the contrast vector. + # The scaling factor is arbitrary, + # but should result in a visually + # sensible scaling of the model fit. + # For a vector of all 1's (i.e. a + # full model fit) this is a no-op. + # + # We also take the absolute value + # of the values in the contrast + # vector, as the parameter estimates + # should be appropriately signed. + contrast = np.array(contrast) + nonzero = sum(~np.isclose(contrast, 0)) + contrast = contrast / np.sqrt((contrast ** 2).sum()) + contrast = np.abs(contrast * np.sqrt(nonzero)) X = self.__design.getDesign(xyz) data = self[x, y, z, :] @@ -340,13 +356,19 @@ class FEATImage(fslimage.Image): pe = self.getPE(i)[x, y, z] modelfit += ev * pe * contrast[i] - # Make sure the full model fit - # has the same mean as the data - if fullmodel: return modelfit - modelfit.mean() + data.mean() - else: return modelfit + # Make sure the model fit has an appropriate mean. + # + # Full model fits should, but won't necessarily, + # have the same mean as the data. + # + # The data in first level analyses is demeaned before + # model fitting, so we need to add it back in. + if fullModel: return modelfit - modelfit.mean() + data.mean() + elif firstLevel: return modelfit - modelfit.mean() + data.mean() + else: return modelfit - def partialFit(self, contrast, xyz, fullmodel=False): + def partialFit(self, contrast, xyz, fullModel=False): """Calculates and returns the partial model fit for the specified contrast vector at the specified voxel. @@ -355,6 +377,6 @@ class FEATImage(fslimage.Image): x, y, z = xyz residuals = self.getResiduals()[x, y, z, :] - modelfit = self.fit(contrast, xyz, fullmodel) + modelfit = self.fit(contrast, xyz, fullModel=fullModel) return residuals + modelfit diff --git a/fsl/data/melodiclabels.py b/fsl/data/melodiclabels.py index 241827914..1b09858a8 100644 --- a/fsl/data/melodiclabels.py +++ b/fsl/data/melodiclabels.py @@ -320,7 +320,8 @@ def loadLabelFile(filename, includeLabel=None, excludeLabel=None): [2, 5, 6, 7] The square brackets may or may not be present, i.e. the - following format is also accepted:: + following format is also accepted (this format is generated + by ICA-AROMA):: 2, 5, 6, 7 -- GitLab