Repository: silx-kit/silx
Issue: [viewer] Display of complex data

The viewer needs to handle complex data.
Trying to display complex data raises an error depending on the selected view.
This is somewhat related to issue #277.

One proposition is to display the modulus of complex (bumpy.absolute), or to add a selection 'filter' to choose what to display.

Changed files:
silx/gui/data/DataViews.py, silx/gui/data/test/test_dataviewer.py

diff --git a/silx/gui/data/DataViews.py b/silx/gui/data/DataViews.py
index 056ff5f81..d8d605a5b 100644
--- a/silx/gui/data/DataViews.py
+++ b/silx/gui/data/DataViews.py
@@ -66,6 +66,21 @@ def _normalizeData(data):
     return data
 
 
+def _normalizeComplex(data):
+    """Returns a normalized complex data.
+
+    If the data is a numpy data with complex, returns the
+    absolute value.
+    Else returns the input data."""
+    if hasattr(data, "dtype"):
+        isComplex = numpy.issubdtype(data.dtype, numpy.complex)
+    else:
+        isComplex = isinstance(data, numbers.Complex)
+    if isComplex:
+        data = numpy.absolute(data)
+    return data
+
+
 class DataInfo(object):
     """Store extracted information from a data"""
 
@@ -74,6 +89,7 @@ class DataInfo(object):
         self.isArray = False
         self.interpretation = None
         self.isNumeric = False
+        self.isComplex = False
         self.isRecord = False
         self.isNXdata = False
         self.shape = tuple()
@@ -103,11 +119,14 @@ class DataInfo(object):
         if hasattr(data, "dtype"):
             self.isNumeric = numpy.issubdtype(data.dtype, numpy.number)
             self.isRecord = data.dtype.fields is not None
+            self.isComplex = numpy.issubdtype(data.dtype, numpy.complex)
         elif self.isNXdata:
             self.isNumeric = numpy.issubdtype(nxd.signal.dtype,
                                               numpy.number)
+            self.isComplex = numpy.issubdtype(nxd.signal.dtype, numpy.complex)
         else:
             self.isNumeric = isinstance(data, numbers.Number)
+            self.isComplex = isinstance(data, numbers.Complex)
             self.isRecord = False
 
         if hasattr(data, "shape"):
@@ -370,6 +389,11 @@ class _Plot1dView(DataView):
         self.getWidget().clear()
         self.__resetZoomNextTime = True
 
+    def normalizeData(self, data):
+        data = DataView.normalizeData(self, data)
+        data = _normalizeComplex(data)
+        return data
+
     def setData(self, data):
         data = self.normalizeData(data)
         self.getWidget().addCurve(legend="data",
@@ -419,6 +443,11 @@ class _Plot2dView(DataView):
         self.getWidget().clear()
         self.__resetZoomNextTime = True
 
+    def normalizeData(self, data):
+        data = DataView.normalizeData(self, data)
+        data = _normalizeComplex(data)
+        return data
+
     def setData(self, data):
         data = self.normalizeData(data)
         self.getWidget().addImage(legend="data",
@@ -483,6 +512,11 @@ class _Plot3dView(DataView):
         self.getWidget().setData(None)
         self.__resetZoomNextTime = True
 
+    def normalizeData(self, data):
+        data = DataView.normalizeData(self, data)
+        data = _normalizeComplex(data)
+        return data
+
     def setData(self, data):
         data = self.normalizeData(data)
         plot = self.getWidget()
@@ -570,6 +604,11 @@ class _StackView(DataView):
         self.getWidget().clear()
         self.__resetZoomNextTime = True
 
+    def normalizeData(self, data):
+        data = DataView.normalizeData(self, data)
+        data = _normalizeComplex(data)
+        return data
+
     def setData(self, data):
         data = self.normalizeData(data)
         self.getWidget().setStack(stack=data, reset=self.__resetZoomNextTime)
diff --git a/silx/gui/data/test/test_dataviewer.py b/silx/gui/data/test/test_dataviewer.py
index 744cf3996..5a0de0b60 100644
--- a/silx/gui/data/test/test_dataviewer.py
+++ b/silx/gui/data/test/test_dataviewer.py
@@ -34,6 +34,7 @@ from contextlib import contextmanager
 import numpy
 from ..DataViewer import DataViewer
 from ..DataViews import DataView
+from .. import DataViews
 
 from silx.gui import qt
 
@@ -41,6 +42,8 @@ from silx.gui.data.DataViewerFrame import DataViewerFrame
 from silx.gui.test.utils import SignalListener
 from silx.gui.test.utils import TestCaseQt
 
+from silx.gui.hdf5.test import _mock
+
 try:
     import h5py
 except ImportError:
@@ -209,7 +212,6 @@ class AbstractDataViewerTests(TestCaseQt):
         self.assertTrue(view not in widget.availableViews())
         self.assertTrue(view not in widget.currentAvailableViews())
 
-
 class TestDataViewer(AbstractDataViewerTests):
     def create_widget(self):
         return DataViewer()
@@ -220,11 +222,58 @@ class TestDataViewerFrame(AbstractDataViewerTests):
         return DataViewerFrame()
 
 
+class TestDataView(TestCaseQt):
+
+    def createComplexData(self):
+        line = [1, 2j, 3+3j, 4]
+        image = [line, line, line, line]
+        cube = [image, image, image, image]
+        data = numpy.array(cube,
+            dtype=numpy.complex)
+        return data
+
+    def createDataViewWithData(self, dataViewClass, data):
+        viewer = dataViewClass(None)
+        widget = viewer.getWidget()
+        viewer.setData(data)
+        return widget
+
+    def testCurveWithComplex(self):
+        data = self.createComplexData()
+        dataViewClass = DataViews._Plot1dView
+        widget = self.createDataViewWithData(dataViewClass, data[0, 0])
+        self.qWaitForWindowExposed(widget)
+
+    def testImageWithComplex(self):
+        data = self.createComplexData()
+        dataViewClass = DataViews._Plot2dView
+        widget = self.createDataViewWithData(dataViewClass, data[0])
+        self.qWaitForWindowExposed(widget)
+
+    def testCubeWithComplex(self):
+        self.skipTest("OpenGL widget not yet tested")
+        try:
+            import silx.gui.plot3d  # noqa
+        except ImportError:
+            self.skipTest("OpenGL not available")
+        data = self.createComplexData()
+        dataViewClass = DataViews._Plot3dView
+        widget = self.createDataViewWithData(dataViewClass, data)
+        self.qWaitForWindowExposed(widget)
+
+    def testImageStackWithComplex(self):
+        data = self.createComplexData()
+        dataViewClass = DataViews._StackView
+        widget = self.createDataViewWithData(dataViewClass, data)
+        self.qWaitForWindowExposed(widget)
+
+
 def suite():
     test_suite = unittest.TestSuite()
     loadTestsFromTestCase = unittest.defaultTestLoader.loadTestsFromTestCase
     test_suite.addTest(loadTestsFromTestCase(TestDataViewer))
     test_suite.addTest(loadTestsFromTestCase(TestDataViewerFrame))
+    test_suite.addTest(loadTestsFromTestCase(TestDataView))
     return test_suite