From 0918a2695ed16d1ffe57c6ff5698a13209fffc9a Mon Sep 17 00:00:00 2001
From: Paul McCarthy <pauld.mccarthy@gmail.com>
Date: Wed, 28 Oct 2015 15:35:06 +0000
Subject: [PATCH] Documentation for melodicimage  and melodicresults modules.
 MelodicImage now automatically sets a TR property, if it can find the data
 file. TimeSeries scales melodic time series by this TR accordingly.

---
 fsl/data/melodicimage.py           |  63 ++++++++++++++--
 fsl/data/melodicresults.py         | 116 +++++++++++++++++++++--------
 fsl/fsleyes/plotting/timeseries.py |  10 ++-
 3 files changed, 149 insertions(+), 40 deletions(-)

diff --git a/fsl/data/melodicimage.py b/fsl/data/melodicimage.py
index 6bdc25480..1456386c9 100644
--- a/fsl/data/melodicimage.py
+++ b/fsl/data/melodicimage.py
@@ -8,30 +8,57 @@
 """This module provides the :class:`MelodicImage` class, an :class:`.Image`
 sub-class which encapsulates data from a MELODIC analysis.
 """
+
+
 import os.path as op
 
+import props
+
 import image          as fslimage
 import melodicresults as melresults
 
 
 class MelodicImage(fslimage.Image):
+    """The ``MelodicImage`` class is an :class:`.Image` which encapsulates
+    the results of a FSL MELODIC analysis. A ``MelodicImage`` corresponds to
+    the spatial component map file, generally called ``melodic_IC.nii.gz``.
+
+    The ``MelodicImage`` class provides a few MELODIC-specific attributes and
+    methods:
+
+    .. autosummary::
+
+       tr
+       getComponentTimeSeries
+       numComponents
+       getDataFile
     """
+
+
+    tr = props.Real(default=1.0)
+    """The TR time of the raw data from which this ``MelodicImage`` was
+    generated. If it is possible to do so, this is automatically initialised
+    from the data file (see the :meth:`getDataFile` method).
     """
+    
 
-    def __init__(self, image, *args, **kwargs):
-        """
-        """
+    def __init__(self, path, *args, **kwargs):
+        """Create a ``MelodicImage``.
 
+        :arg path: A path specifying the ``melodic_IC`` image file, or the
+                   ``.ica`` directory.
 
-        if op.isdir(image):
+        All other arguments are passed through to the :meth:`.Image.__init__`
+        method.
+        """
 
-            dirname  = image
+        if op.isdir(path):
+            dirname  = path
             filename = 'melodic_IC'
 
-
         else:
-            dirname  = op.dirname( image)
-            filename = op.basename(image)
+            dirname  = op.dirname( path)
+            filename = op.basename(path)
 
         dirname = dirname.rstrip(op.sep)
 
@@ -48,12 +75,32 @@ class MelodicImage(fslimage.Image):
                                 *args,
                                 **kwargs)
 
+        self.__meldir = dirname
         self.__melmix = melresults.getComponentTimeSeries(dirname)
 
+        # Automatically set the
+        # TR value if possible
+        dataFile = self.getDataFile()
+
+        if dataFile is not None: 
+            dataImage = fslimage.Image(dataFile, loadData=False)
+            if dataImage.is4DImage():
+                self.tr = dataImage.pixdim[3]
+
         
     def getComponentTimeSeries(self, component):
+        """Returns the time course for the specified (0-indexed) component. """
         return self.__melmix[:, component]
 
 
     def numComponents(self):
+        """Returns the number of components in this ``MelodicImage``. """
         return self.shape[3]
+
+
+    def getDataFile(self):
+        """Returns the file name of the data image from which this
+        ``MelodicImage`` was generated, if possible. See the
+        :func:`.melodicresults.getDataFile` function.
+        """
+        return melresults.getDataFile(self.__meldir)
diff --git a/fsl/data/melodicresults.py b/fsl/data/melodicresults.py
index d05a25ab3..f020003fb 100644
--- a/fsl/data/melodicresults.py
+++ b/fsl/data/melodicresults.py
@@ -15,49 +15,46 @@ following functions are provided:
 
    isMelodicDir
    getMelodicDir
+   getTopLevelAnalysisDir
+   getDataFile
    getICFile
+   getMixFile
    getNumComponents
    getComponentTimeSeries
 """
 
 
-import            os 
 import os.path as op
 import numpy   as np
 
-import fsl.data.image as fslimage
+import fsl.data.image       as fslimage
+import fsl.data.featresults as featresults
 
 
 def isMelodicDir(path):
-    """
+    """Returns ``True`` if the given path looks like it is contained within
+    a MELODIC directory, ``False`` otherwise. 
     """
 
     # Must be named *.ica or *.gica
-    meldir = getMelodicDir(path)
-
-    if meldir is None:
-        return False
-
-    # Must contain an image file called melodic_IC
-    try:
-        fslimage.addExt(op.join(meldir, 'melodic_IC'), mustExist=True)
-    except ValueError:
-        return False
-
-    # Must contain a file called melodic_mix
-    if not op.exists(op.join(meldir, 'melodic_mix')):
-        return False
-
-    return True
+    return getMelodicDir(path) is not None
 
     
 def getMelodicDir(path):
-    """
+    """Returns the MELODIC directory in which the given path is contained,
+    or ``None`` if it is not contained within a MELODIC directory. A melodic
+    directory:
+
+      - Must be named ``*.ica`` or ``*.gica``
+      - Must contain a file called ``melodic_IC.nii.gz``
+      - Must contain a file called ``melodic_mix``.
     """
 
     # TODO This code is identical to featresults.getFEATDir.
     # Can you generalise it and put it somewhere in fsl.utils?
 
+    path     = op.abspath(path)
+
     sufs     = ['.ica', '.gica']
     idxs     = [(path.rfind(s), s) for s in sufs]
     idx, suf = max(idxs, key=lambda (i, s): i)
@@ -66,28 +63,88 @@ def getMelodicDir(path):
         return None
 
     idx  += len(suf)
-    path  = path[:idx]
+    path  = path[:idx].rstrip(op.sep)
 
-    if path.endswith(suf) or path.endswith('{}{}'.format(suf, op.sep)):
-        return path
+    if not path.endswith(suf):
+        return None
+
+    # Must contain an image file called melodic_IC
+    try:
+        fslimage.addExt(op.join(path, 'melodic_IC'), mustExist=True)
+    except ValueError:
+        return None
+
+    # Must contain a file called melodic_mix
+    if not op.exists(op.join(path, 'melodic_mix')):
+        return None
                                            
-    return None 
+    return path
 
 
-def getICFile(meldir):
+def getTopLevelAnalysisDir(path):
+    """If the given path is a MELODIC directory, and it is contained within
+    a FEAT directory, or another MELODIC directory, the path to the latter
+    directory is returned. Otherwise, ``None`` is returned.
     """
+
+    meldir = getMelodicDir(path)
+    sufs   =  ['.feat', '.gfeat', '.ica', '.gica']
+    
+    if meldir is None:
+        return None
+
+    if featresults.isFEATDir(meldir):
+        return featresults.getFEATDir(meldir)
+
+    parentDir = op.dirname(meldir)
+    parentDir = parentDir.rstrip(op.sep)
+
+    if not any([parentDir.endswith(s) for s in sufs]):
+        return None
+
+    # Must contain a file called filtered_func_data.nii.gz
+    dataFile = op.join(parentDir, 'filtered_func_data')
+
+    try:
+        dataFile = fslimage.addExt(dataFile, mustExist=True)
+    except ValueError:
+        return None
+
+    return parentDir
+
+    
+def getDataFile(meldir):
+    """If the given melodic directory is contained within another analysis
+    directory, the path to the data file is returned. Otherwise ``None`` is
+    returned.
     """
+
+    topDir = getTopLevelAnalysisDir(meldir)
+
+    if topDir is None:
+        return None
+
+    dataFile = op.join(topDir, 'filtered_func_data')
+
+    try:
+        return fslimage.addExt(dataFile, mustExist=True)
+    except ValueError:
+        return None
+
+
+def getICFile(meldir):
+    """Returns the path to the melodic IC image. """
     return fslimage.addExt(op.join(meldir, 'melodic_IC'))
 
 
 def getMixFile(meldir):
-    """
-    """
+    """Returns the path to the melodic mix file. """
     return op.join(meldir, 'melodic_mix')
 
 
 def getNumComponents(meldir):
-    """
+    """Returns the number of components generated in the melodic analysis
+    contained in the given directrory.
     """
 
     icImg = fslimage.Image(getICFile(meldir), loadData=False)
@@ -95,7 +152,8 @@ def getNumComponents(meldir):
 
 
 def getComponentTimeSeries(meldir):
-    """
+    """Returns a ``numpy`` array containing the melodic mix for the given
+    directory.
     """
 
     mixfile = getMixFile(meldir)
diff --git a/fsl/fsleyes/plotting/timeseries.py b/fsl/fsleyes/plotting/timeseries.py
index 8d484aa16..094d450c8 100644
--- a/fsl/fsleyes/plotting/timeseries.py
+++ b/fsl/fsleyes/plotting/timeseries.py
@@ -26,8 +26,9 @@ import numpy as np
 
 import props
 
-import                     dataseries
-import fsl.data.strings as strings
+import                          dataseries
+import fsl.data.strings      as strings
+import fsl.data.melodicimage as fslmelimage
 
 
 class TimeSeries(dataseries.DataSeries):
@@ -90,7 +91,10 @@ class TimeSeries(dataseries.DataSeries):
         ydata = np.array(ydata, dtype=np.float32)
 
         if self.tsPanel.usePixdim:
-            xdata *= self.overlay.pixdim[3]
+            if isinstance(self.overlay, fslmelimage.MelodicImage):
+                xdata *= self.overlay.tr
+            else:
+                xdata *= self.overlay.pixdim[3]
         
         if self.tsPanel.plotMode == 'demean':
             ydata = ydata - ydata.mean()
-- 
GitLab