From 118dbbc8e2ae1f415172fc79620f1d8269e277aa Mon Sep 17 00:00:00 2001
From: Paul McCarthy <pauld.mccarthy@gmail.com>
Date: Wed, 8 Jul 2015 16:53:06 +0100
Subject: [PATCH] Ability  to plot 'reduced' data (with respect to a specific
 PE/COPE), and residuals.

---
 fsl/data/featimage.py                         | 33 +++++--
 fsl/data/strings.py                           |  2 +
 .../controls/timeseriescontrolpanel.py        | 13 +++
 fsl/fslview/views/timeseriespanel.py          | 89 +++++++++++++++++--
 4 files changed, 124 insertions(+), 13 deletions(-)

diff --git a/fsl/data/featimage.py b/fsl/data/featimage.py
index 384dfd0a5..b5eb96ea8 100644
--- a/fsl/data/featimage.py
+++ b/fsl/data/featimage.py
@@ -154,6 +154,7 @@ class FEATImage(fslimage.Image):
         self.__contrasts     = cons
         self.__settings      = settings
 
+        self.__residuals     =  None
         self.__pes           = [None] * self.numEVs()
         self.__copes         = [None] * self.numContrasts()
 
@@ -183,24 +184,37 @@ class FEATImage(fslimage.Image):
         return [list(c) for c in self.__contrasts]
 
 
-    def __getPEFile(self, prefix, ev):
-        prefix = op.join(self.__featDir, 'stats', '{}{}'.format(
-            prefix, ev + 1))
+    def __getStatsFile(self, prefix, ev=None):
+
+        if ev is not None: prefix = '{}{}'.format(prefix, ev + 1)
+
+        prefix = op.join(self.__featDir, 'stats', prefix)
+        
         return glob.glob('{}.*'.format(prefix))[0]
 
 
     def getPE(self, ev):
 
         if self.__pes[ev] is None:
-            pefile = self.__getPEFile('pe', ev)
+            pefile = self.__getStatsFile('pe', ev)
             self.__pes[ev] = nib.load(pefile).get_data()
 
         return self.__pes[ev]
 
+
+    def getResiduals(self):
+        
+        if self.__residuals is None:
+            resfile          = self.__getStatsFile('res4d')
+            self.__residuals = nib.load(resfile).get_data()
+        
+        return self.__residuals
+
     
     def getCOPE(self, num):
+        
         if self.__copes[num] is None:
-            copefile = self.__getPEFile('cope', num)
+            copefile = self.__getStatsFile('cope', num)
             self.__copes[num] = nib.load(copefile).get_data()
 
         return self.__copes[num] 
@@ -229,6 +243,15 @@ class FEATImage(fslimage.Image):
 
         return modelfit + data.mean()
 
+
+    def reducedData(self, xyz, contrast, fullmodel=False):
+
+        x, y, z   = xyz
+        residuals = self.getResiduals()[x, y, z, :]
+        modelfit  = self.fit(contrast, xyz, fullmodel)
+
+        return residuals + modelfit
+
     
     # def getThresholdedZStats(self):
     #     pass
diff --git a/fsl/data/strings.py b/fsl/data/strings.py
index 01a649224..d3f168057 100644
--- a/fsl/data/strings.py
+++ b/fsl/data/strings.py
@@ -290,6 +290,8 @@ properties = TypeDict({
     'FEATTimeSeries.plotFullModelFit' : 'Plot full model fit',
     'FEATTimeSeries.plotPEFits'       : 'Plot PE{} fit',
     'FEATTimeSeries.plotCOPEFits'     : 'Plot COPE{} fit ({})',
+    'FEATTimeSeries.plotResiduals'    : 'Show residuals',
+    'FEATTimeSeries.reduceAgainst'    : 'Reduce data against',
 
     'OrthoEditProfile.selectionSize'          : 'Selection size',
     'OrthoEditProfile.selectionIs3D'          : '3D selection',
diff --git a/fsl/fslview/controls/timeseriescontrolpanel.py b/fsl/fslview/controls/timeseriescontrolpanel.py
index bdec0f8e0..f3302a830 100644
--- a/fsl/fslview/controls/timeseriescontrolpanel.py
+++ b/fsl/fslview/controls/timeseriescontrolpanel.py
@@ -198,12 +198,25 @@ class TimeSeriesControlPanel(fslpanel.FSLViewPanel):
                 display.name))
 
         full    = props.makeWidget(     self.__widgets, ts, 'plotFullModelFit')
+        res     = props.makeWidget(     self.__widgets, ts, 'plotResiduals')
         pes     = props.makeListWidgets(self.__widgets, ts, 'plotPEFits')
         copes   = props.makeListWidgets(self.__widgets, ts, 'plotCOPEFits')
+        reduce  = props.makeWidget(     self.__widgets, ts, 'reduceAgainst')
+        
         self.__widgets.AddWidget(
             full,
             displayName=strings.properties[ts, 'plotFullModelFit'],
             groupName='currentFEATSettings')
+        
+        self.__widgets.AddWidget(
+            res,
+            displayName=strings.properties[ts, 'plotResiduals'],
+            groupName='currentFEATSettings')
+        
+        self.__widgets.AddWidget(
+            reduce,
+            displayName=strings.properties[ts, 'reduceAgainst'],
+            groupName='currentFEATSettings') 
 
         for i, pe in enumerate(pes):
             self.__widgets.AddWidget(
diff --git a/fsl/fslview/views/timeseriespanel.py b/fsl/fslview/views/timeseriespanel.py
index e5c26f64c..4ddb0a88a 100644
--- a/fsl/fslview/views/timeseriespanel.py
+++ b/fsl/fslview/views/timeseriespanel.py
@@ -56,9 +56,16 @@ class TimeSeries(plotpanel.DataSeries):
         return True
 
         
-    def getData(self):
-        ydata = np.array( self.data,  dtype=np.float32)
-        xdata = np.arange(len(ydata), dtype=np.float32)
+    def getData(self, xdata=None, ydata=None):
+        """
+        
+        :arg xdata:
+        :arg ydata: Used by subclasses in case they have already done some
+                    processing on the data.
+        """
+
+        if xdata is None: xdata = np.arange(len(self.data), dtype=np.float32)
+        if ydata is None: ydata = np.array(     self.data,  dtype=np.float32)
 
         if self.tsPanel.usePixdim:
             xdata *= self.overlay.pixdim[3]
@@ -77,19 +84,28 @@ class FEATTimeSeries(TimeSeries):
 
     
     plotFullModelFit = props.Boolean(default=False)
+    plotResiduals    = props.Boolean(default=False)
     plotPEFits       = props.List(props.Boolean(default=False))
     plotCOPEFits     = props.List(props.Boolean(default=False))
-
-    # TODO 'None', or any PE/COPE
-    reduceDataAgainst = props.Choice()
+    reduceAgainst    = props.Choice()
 
 
     def __init__(self, *args, **kwargs):
         TimeSeries.__init__(self, *args, **kwargs)
         self.name = '{}_{}'.format(type(self).__name__, id(self))
 
-        numEVs   = self.overlay.numEVs()
-        numCOPEs = self.overlay.numContrasts()
+        numEVs    = self.overlay.numEVs()
+        numCOPEs  = self.overlay.numContrasts()
+        copeNames = self.overlay.contrastNames()
+        
+        reduceOpts = ['none'] + \
+                     ['PE{}'.format(i + 1) for i in range(numEVs)]
+
+        for i in range(numCOPEs):
+            name = 'COPE{} ({})'.format(i + 1, copeNames[i])
+            reduceOpts.append(name)
+
+        self.getProp('reduceAgainst').setChoices(reduceOpts, instance=self)
 
         for i in range(numEVs):
             self.plotPEFits.append(False)
@@ -98,12 +114,17 @@ class FEATTimeSeries(TimeSeries):
             self.plotCOPEFits.append(False) 
 
         self.__fullModelTs =  None
+        self.__resTs       =  None
         self.__peTs        = [None] * numEVs
         self.__copeTs      = [None] * numCOPEs
         
         self.addListener('plotFullModelFit',
                          self.name,
                          self.__plotFullModelFitChanged)
+
+        self.addListener('plotResiduals',
+                         self.name,
+                         self.__plotResidualsChanged)
         
         for i, plotPEFit in enumerate(
                 self.plotPEFits.getPropertyValueList()):
@@ -121,6 +142,28 @@ class FEATTimeSeries(TimeSeries):
 
             plotCOPEFit.addListener(self.name, onChange)
 
+            
+    def getData(self):
+        
+        reduce = self.reduceAgainst
+
+        if reduce == 'none':
+            data = None
+
+        else:
+            idx    = int(reduce.split()[0][-1]) - 1
+            numEVs = self.overlay.numEVs()
+
+            if reduce.startswith('PE'):
+                contrast      = [0] * numEVs
+                contrast[idx] = 1
+            else:
+                contrast      = self.overlay.contrasts()[idx]
+ 
+            data = self.overlay.reducedData(self.coords, contrast, False)
+
+        return TimeSeries.getData(self, ydata=data)
+
 
     def __copy__(self):
         copy = type(self)(self.tsPanel, self.overlay, self.coords)
@@ -148,6 +191,9 @@ class FEATTimeSeries(TimeSeries):
         if self.plotFullModelFit:
             modelts.append(self.__fullModelTs)
 
+        if self.plotResiduals:
+            modelts.append(self.__resTs)
+
         for i in range(self.overlay.numEVs()):
             if self.plotPEFits[i]:
                 modelts.append(self.__peTs[i])
@@ -158,6 +204,25 @@ class FEATTimeSeries(TimeSeries):
         
         return modelts
 
+
+    def __plotResidualsChanged(self, *a):
+        if not self.plotResiduals:
+            self.__resTs = None
+            return
+
+        rts = FEATResidualTimeSeries(
+            self.tsPanel,
+            self.overlay,
+            self.coords)
+
+        rts.colour    = (0.8, 0.4, 0)
+        rts.alpha     = self.alpha
+        rts.label     = self.label
+        rts.lineWidth = self.lineWidth
+        rts.lineStyle = self.lineStyle
+
+        self.__resTs = rts
+            
     
     def __plotCOPEFitChanged(self, copenum):
         if not self.plotCOPEFits[copenum]:
@@ -237,6 +302,14 @@ class FEATTimeSeries(TimeSeries):
         return True
 
 
+class FEATResidualTimeSeries(TimeSeries):
+    def getData(self):
+        x, y, z = self.coords
+        data    = self.overlay.getResiduals()[x, y, z, :]
+        
+        return TimeSeries.getData(self, ydata=data)
+            
+
 class FEATModelFitTimeSeries(TimeSeries):
     
 
-- 
GitLab