summaryrefslogtreecommitdiff
path: root/silx/gui/plot3d/items
diff options
context:
space:
mode:
Diffstat (limited to 'silx/gui/plot3d/items')
-rw-r--r--silx/gui/plot3d/items/__init__.py4
-rw-r--r--silx/gui/plot3d/items/mesh.py5
-rw-r--r--silx/gui/plot3d/items/mixins.py18
-rw-r--r--silx/gui/plot3d/items/scatter.py108
-rw-r--r--silx/gui/plot3d/items/volume.py308
5 files changed, 323 insertions, 120 deletions
diff --git a/silx/gui/plot3d/items/__init__.py b/silx/gui/plot3d/items/__init__.py
index 58eee9c..5810618 100644
--- a/silx/gui/plot3d/items/__init__.py
+++ b/silx/gui/plot3d/items/__init__.py
@@ -34,10 +34,10 @@ __date__ = "15/11/2017"
from .core import DataItem3D, Item3D, GroupItem, GroupWithAxesItem # noqa
from .core import ItemChangedType, Item3DChangedType # noqa
-from .mixins import (ColormapMixIn, InterpolationMixIn, # noqa
+from .mixins import (ColormapMixIn, ComplexMixIn, InterpolationMixIn, # noqa
PlaneMixIn, SymbolMixIn) # noqa
from .clipplane import ClipPlane # noqa
from .image import ImageData, ImageRgba # noqa
from .mesh import Mesh, ColormapMesh, Box, Cylinder, Hexagon # noqa
from .scatter import Scatter2D, Scatter3D # noqa
-from .volume import ScalarField3D # noqa
+from .volume import ComplexField3D, ScalarField3D # noqa
diff --git a/silx/gui/plot3d/items/mesh.py b/silx/gui/plot3d/items/mesh.py
index d3f5e38..3577dbf 100644
--- a/silx/gui/plot3d/items/mesh.py
+++ b/silx/gui/plot3d/items/mesh.py
@@ -35,6 +35,7 @@ __date__ = "17/07/2018"
import logging
import numpy
+from ... import _glutils as glu
from ..scene import primitives, utils, function
from ..scene.transform import Rotate
from .core import DataItem3D, ItemChangedType
@@ -168,7 +169,7 @@ class _MeshBase(DataItem3D):
_logger.warning("Unsupported draw mode: %s" % mode)
return None
- trianglesIndices, t, barycentric = utils.segmentTrianglesIntersection(
+ trianglesIndices, t, barycentric = glu.segmentTrianglesIntersection(
rayObject, triangles)
if len(trianglesIndices) == 0:
@@ -494,7 +495,7 @@ class _CylindricalVolume(DataItem3D):
positions = self._mesh.getAttribute('position', copy=False)
triangles = positions.reshape(-1, 3, 3) # 'triangle' draw mode
- trianglesIndices, t = utils.segmentTrianglesIntersection(
+ trianglesIndices, t = glu.segmentTrianglesIntersection(
rayObject, triangles)[:2]
if len(trianglesIndices) == 0:
diff --git a/silx/gui/plot3d/items/mixins.py b/silx/gui/plot3d/items/mixins.py
index 40b8438..b355627 100644
--- a/silx/gui/plot3d/items/mixins.py
+++ b/silx/gui/plot3d/items/mixins.py
@@ -38,6 +38,7 @@ from silx.math.combo import min_max
from ...plot.items.core import ItemMixInBase
from ...plot.items.core import ColormapMixIn as _ColormapMixIn
from ...plot.items.core import SymbolMixIn as _SymbolMixIn
+from ...plot.items.core import ComplexMixIn as _ComplexMixIn
from ...colors import rgba
from ..scene import primitives
@@ -139,8 +140,9 @@ class ColormapMixIn(_ColormapMixIn):
self._dataRange = dataRange
- if self.getColormap().isAutoscale():
- self._syncSceneColormap()
+ colormap = self.getColormap()
+ if None in (colormap.getVMin(), colormap.getVMax()):
+ self._colormapChanged()
def _getDataRange(self):
"""Returns the data range as used in the scene for colormap
@@ -173,6 +175,18 @@ class ColormapMixIn(_ColormapMixIn):
self.__sceneColormap.range_ = range_
+class ComplexMixIn(_ComplexMixIn):
+ __doc__ = _ComplexMixIn.__doc__ # Reuse docstring
+
+ _SUPPORTED_COMPLEX_MODES = (
+ _ComplexMixIn.ComplexMode.REAL,
+ _ComplexMixIn.ComplexMode.IMAGINARY,
+ _ComplexMixIn.ComplexMode.ABSOLUTE,
+ _ComplexMixIn.ComplexMode.PHASE,
+ _ComplexMixIn.ComplexMode.SQUARE_AMPLITUDE)
+ """Overrides supported ComplexMode"""
+
+
class SymbolMixIn(_SymbolMixIn):
"""Mix-in class for symbol and symbolSize properties for Item3D"""
diff --git a/silx/gui/plot3d/items/scatter.py b/silx/gui/plot3d/items/scatter.py
index b7bcd09..e8ffee1 100644
--- a/silx/gui/plot3d/items/scatter.py
+++ b/silx/gui/plot3d/items/scatter.py
@@ -31,14 +31,19 @@ __authors__ = ["T. Vincent"]
__license__ = "MIT"
__date__ = "15/11/2017"
-import collections
+try:
+ from collections import abc
+except ImportError: # Python2 support
+ import collections as abc
import logging
-import sys
import numpy
from ....utils.deprecation import deprecated
+from ... import _glutils as glu
+from ...plot._utils.delaunay import delaunay
from ..scene import function, primitives, utils
+from ...plot.items import ScatterVisualizationMixIn
from .core import DataItem3D, Item3DChangedType, ItemChangedType
from .mixins import ColormapMixIn, SymbolMixIn
from ._pick import PickingResult
@@ -213,16 +218,19 @@ class Scatter3D(DataItem3D, ColormapMixIn, SymbolMixIn):
return None
-class Scatter2D(DataItem3D, ColormapMixIn, SymbolMixIn):
+class Scatter2D(DataItem3D, ColormapMixIn, SymbolMixIn,
+ ScatterVisualizationMixIn):
"""2D scatter data with settable visualization mode.
:param parent: The View widget this item belongs to.
"""
_VISUALIZATION_PROPERTIES = {
- 'points': ('symbol', 'symbolSize'),
- 'lines': ('lineWidth',),
- 'solid': (),
+ ScatterVisualizationMixIn.Visualization.POINTS:
+ ('symbol', 'symbolSize'),
+ ScatterVisualizationMixIn.Visualization.LINES:
+ ('lineWidth',),
+ ScatterVisualizationMixIn.Visualization.SOLID: (),
}
"""Dict {visualization mode: property names used in this mode}"""
@@ -230,8 +238,8 @@ class Scatter2D(DataItem3D, ColormapMixIn, SymbolMixIn):
DataItem3D.__init__(self, parent=parent)
ColormapMixIn.__init__(self)
SymbolMixIn.__init__(self)
+ ScatterVisualizationMixIn.__init__(self)
- self._visualizationMode = 'points'
self._heightMap = False
self._lineWidth = 1.
@@ -254,48 +262,14 @@ class Scatter2D(DataItem3D, ColormapMixIn, SymbolMixIn):
child.marker = symbol
child.setAttribute('size', size, copy=True)
- elif event == ItemChangedType.VISIBLE:
+ elif event is ItemChangedType.VISIBLE:
# TODO smart update?, need dirty flags
self._updateScene()
- super(Scatter2D, self)._updated(event)
-
- def supportedVisualizations(self):
- """Returns the list of supported visualization modes.
-
- See :meth:`setVisualizationModes`
-
- :rtype: tuple of str
- """
- return tuple(self._VISUALIZATION_PROPERTIES.keys())
-
- def setVisualization(self, mode):
- """Set the visualization mode of the data.
-
- Supported visualization modes are:
-
- - 'points': For scatter plot representation
- - 'lines': For Delaunay tessellation-based wireframe representation
- - 'solid': For Delaunay tessellation-based solid surface representation
-
- :param str mode: Mode of representation to use
- """
- mode = str(mode)
- assert mode in self.supportedVisualizations()
-
- if mode != self.getVisualization():
- self._visualizationMode = mode
+ elif event is ItemChangedType.VISUALIZATION_MODE:
self._updateScene()
- self._updated(ItemChangedType.VISUALIZATION_MODE)
- def getVisualization(self):
- """Returns the current visualization mode.
-
- See :meth:`setVisualization`
-
- :rtype: str
- """
- return self._visualizationMode
+ super(Scatter2D, self)._updated(event)
def isPropertyEnabled(self, name, visualization=None):
"""Returns true if the property is used with visualization mode.
@@ -374,7 +348,7 @@ class Scatter2D(DataItem3D, ColormapMixIn, SymbolMixIn):
y, copy=copy, dtype=numpy.float32, order='C').reshape(-1)
assert len(x) == len(y)
- if isinstance(value, collections.Iterable):
+ if isinstance(value, abc.Iterable):
value = numpy.array(
value, copy=copy, dtype=numpy.float32, order='C').reshape(-1)
assert len(value) == len(x)
@@ -503,7 +477,7 @@ class Scatter2D(DataItem3D, ColormapMixIn, SymbolMixIn):
trianglesIndices = self._cachedTrianglesIndices.reshape(-1, 3)
triangles = points[trianglesIndices, :3]
- selectedIndices, t, barycentric = utils.segmentTrianglesIntersection(
+ selectedIndices, t, barycentric = glu.segmentTrianglesIntersection(
rayObject, triangles)
closest = numpy.argmax(barycentric, axis=1)
@@ -542,14 +516,14 @@ class Scatter2D(DataItem3D, ColormapMixIn, SymbolMixIn):
numpy.ones_like(xData)))
mode = self.getVisualization()
- if mode == 'points':
+ if mode is self.Visualization.POINTS:
# TODO issue with symbol size: using pixel instead of points
# Get "corrected" symbol size
_, threshold = self._getSceneSymbol()
return self._pickPoints(
context, points, threshold=max(3., threshold))
- elif mode == 'lines':
+ elif mode is self.Visualization.LINES:
# Picking only at point
return self._pickPoints(context, points, threshold=5.)
@@ -569,7 +543,7 @@ class Scatter2D(DataItem3D, ColormapMixIn, SymbolMixIn):
mode = self.getVisualization()
heightMap = self.isHeightMap()
- if mode == 'points':
+ if mode is self.Visualization.POINTS:
z = value if heightMap else 0.
symbol, size = self._getSceneSymbol()
primitive = primitives.Points(
@@ -582,35 +556,19 @@ class Scatter2D(DataItem3D, ColormapMixIn, SymbolMixIn):
# TODO run delaunay in a thread
# Compute lines/triangles indices if not cached
if self._cachedTrianglesIndices is None:
- coordinates = numpy.array((x, y)).T
-
- if len(coordinates) > 3:
- # Enough points to try a Delaunay tesselation
-
- # Lazy loading of Delaunay
- from silx.third_party.scipy_spatial import Delaunay as _Delaunay
-
- try:
- tri = _Delaunay(coordinates)
- except RuntimeError:
- _logger.error("Delaunay tesselation failed: %s",
- sys.exc_info()[1])
- return None
-
- self._cachedTrianglesIndices = numpy.ravel(
- tri.simplices.astype(numpy.uint32))
-
- else:
- # 3 or less points: Draw one triangle
- self._cachedTrianglesIndices = \
- numpy.arange(3, dtype=numpy.uint32) % len(coordinates)
-
- if mode == 'lines' and self._cachedLinesIndices is None:
+ triangulation = delaunay(x, y)
+ if triangulation is None:
+ return None
+ self._cachedTrianglesIndices = numpy.ravel(
+ triangulation.simplices.astype(numpy.uint32))
+
+ if (mode is self.Visualization.LINES and
+ self._cachedLinesIndices is None):
# Compute line indices
self._cachedLinesIndices = utils.triangleToLineIndices(
self._cachedTrianglesIndices, unicity=True)
- if mode == 'lines':
+ if mode is self.Visualization.LINES:
indices = self._cachedLinesIndices
renderMode = 'lines'
else:
@@ -627,7 +585,7 @@ class Scatter2D(DataItem3D, ColormapMixIn, SymbolMixIn):
# TODO option to enable/disable light, cache normals
# TODO smooth surface
- if mode == 'solid':
+ if mode is self.Visualization.SOLID:
if heightMap:
coordinates = coordinates[indices]
if len(value) > 1:
diff --git a/silx/gui/plot3d/items/volume.py b/silx/gui/plot3d/items/volume.py
index 08ad02a..ae91e82 100644
--- a/silx/gui/plot3d/items/volume.py
+++ b/silx/gui/plot3d/items/volume.py
@@ -38,13 +38,15 @@ import numpy
from silx.math.combo import min_max
from silx.math.marchingcubes import MarchingCubes
+from ....utils.proxy import docstring
+from ... import _glutils as glu
from ... import qt
from ...colors import rgba
from ..scene import cutplane, primitives, transform, utils
from .core import BaseNodeItem, Item3D, ItemChangedType, Item3DChangedType
-from .mixins import ColormapMixIn, InterpolationMixIn, PlaneMixIn
+from .mixins import ColormapMixIn, ComplexMixIn, InterpolationMixIn, PlaneMixIn
from ._pick import PickingResult
@@ -60,12 +62,13 @@ class CutPlane(Item3D, ColormapMixIn, InterpolationMixIn, PlaneMixIn):
def __init__(self, parent):
plane = cutplane.CutPlane(normal=(0, 1, 0))
- Item3D.__init__(self, parent=parent)
+ Item3D.__init__(self, parent=None)
ColormapMixIn.__init__(self)
InterpolationMixIn.__init__(self)
PlaneMixIn.__init__(self, plane=plane)
self._dataRange = None
+ self._data = None
self._getScenePrimitive().children = [plane]
@@ -73,20 +76,53 @@ class CutPlane(Item3D, ColormapMixIn, InterpolationMixIn, PlaneMixIn):
ColormapMixIn._setSceneColormap(self, plane.colormap)
InterpolationMixIn._setPrimitive(self, plane)
- parent.sigItemChanged.connect(self._parentChanged)
+ self.setParent(parent)
+
+ def _updateData(self, data, range_):
+ """Update used dataset
+
+ No copy is made.
+
+ :param Union[numpy.ndarray[float],None] data: The dataset
+ :param Union[List[float],None] range_:
+ (min, min positive, max) values
+ """
+ self._data = None if data is None else numpy.array(data, copy=False)
+ self._getPlane().setData(self._data, copy=False)
+
+ # Store data range info as 3-tuple of values
+ self._dataRange = range_
+ self._setRangeFromData(
+ None if self._dataRange is None else numpy.array(self._dataRange))
+
+ self._updated(ItemChangedType.DATA)
+
+ def _syncDataWithParent(self):
+ """Synchronize this instance data with that of its parent"""
+ parent = self.parent()
+ if parent is None:
+ data, range_ = None, None
+ else:
+ data = parent.getData(copy=False)
+ range_ = parent.getDataRange()
+ self._updateData(data, range_)
def _parentChanged(self, event):
"""Handle data change in the parent this plane belongs to"""
if event == ItemChangedType.DATA:
- data = self.sender().getData(copy=False)
- self._getPlane().setData(data, copy=False)
+ self._syncDataWithParent()
+
+ def setParent(self, parent):
+ oldParent = self.parent()
+ if isinstance(oldParent, Item3D):
+ oldParent.sigItemChanged.disconnect(self._parentChanged)
- # Store data range info as 3-tuple of values
- self._dataRange = self.sender().getDataRange()
- self._setRangeFromData(
- None if self._dataRange is None else numpy.array(self._dataRange))
+ super(CutPlane, self).setParent(parent)
- self._updated(ItemChangedType.DATA)
+ if isinstance(parent, Item3D):
+ parent.sigItemChanged.connect(self._parentChanged)
+
+ self._syncDataWithParent()
# Colormap
@@ -114,8 +150,9 @@ class CutPlane(Item3D, ColormapMixIn, InterpolationMixIn, PlaneMixIn):
positive min is NaN if no data is positive.
:return: (min, positive min, max) or None.
+ :rtype: Union[List[float],None]
"""
- return self._dataRange
+ return None if self._dataRange is None else tuple(self._dataRange)
def getData(self, copy=True):
"""Return 3D dataset.
@@ -125,8 +162,10 @@ class CutPlane(Item3D, ColormapMixIn, InterpolationMixIn, PlaneMixIn):
False to get the internal data (DO NOT modify!)
:return: The data set (or None if not set)
"""
- parent = self.parent()
- return None if parent is None else parent.getData(copy=copy)
+ if self._data is None:
+ return None
+ else:
+ return numpy.array(self._data, copy=copy)
def _pickFull(self, context):
"""Perform picking in this item at given widget position.
@@ -172,18 +211,38 @@ class Isosurface(Item3D):
"""
def __init__(self, parent):
- Item3D.__init__(self, parent=parent)
- assert isinstance(parent, ScalarField3D)
- parent.sigItemChanged.connect(self._scalarField3DChanged)
+ Item3D.__init__(self, parent=None)
+ self._data = None
self._level = float('nan')
self._autoLevelFunction = None
self._color = rgba('#FFD700FF')
+ self.setParent(parent)
+
+ def _syncDataWithParent(self):
+ """Synchronize this instance data with that of its parent"""
+ parent = self.parent()
+ if parent is None:
+ self._data = None
+ else:
+ self._data = parent.getData(copy=False)
self._updateScenePrimitive()
- def _scalarField3DChanged(self, event):
- """Handle parent's ScalarField3D sigItemChanged"""
+ def _parentChanged(self, event):
+ """Handle data change in the parent this isosurface belongs to"""
if event == ItemChangedType.DATA:
- self._updateScenePrimitive()
+ self._syncDataWithParent()
+
+ def setParent(self, parent):
+ oldParent = self.parent()
+ if isinstance(oldParent, Item3D):
+ oldParent.sigItemChanged.disconnect(self._parentChanged)
+
+ super(Isosurface, self).setParent(parent)
+
+ if isinstance(parent, Item3D):
+ parent.sigItemChanged.connect(self._parentChanged)
+
+ self._syncDataWithParent()
def getData(self, copy=True):
"""Return 3D dataset.
@@ -193,8 +252,10 @@ class Isosurface(Item3D):
False to get the internal data (DO NOT modify!)
:return: The data set (or None if not set)
"""
- parent = self.parent()
- return None if parent is None else parent.getData(copy=copy)
+ if self._data is None:
+ return None
+ else:
+ return numpy.array(self._data, copy=copy)
def getLevel(self):
"""Return the level of this iso-surface (float)"""
@@ -349,7 +410,7 @@ class Isosurface(Item3D):
mc = MarchingCubes(data.reshape(2, 2, 2), isolevel=level)
points = mc.get_vertices() + currentBin
triangles = points[mc.get_indices()]
- t = utils.segmentTrianglesIntersection(rayObject, triangles)[1]
+ t = glu.segmentTrianglesIntersection(rayObject, triangles)[1]
t = numpy.unique(t) # Duplicates happen on triangle edges
if len(t) != 0:
# Compute intersection points and get closest data point
@@ -372,6 +433,12 @@ class ScalarField3D(BaseNodeItem):
:param parent: The View widget this item belongs to.
"""
+ _CutPlane = CutPlane
+ """CutPlane class associated to this class"""
+
+ _Isosurface = Isosurface
+ """Isosurface classe associated to this class"""
+
def __init__(self, parent=None):
BaseNodeItem.__init__(self, parent=parent)
@@ -385,7 +452,7 @@ class ScalarField3D(BaseNodeItem):
self._data = None
self._dataRange = None
- self._cutPlane = CutPlane(parent=self)
+ self._cutPlane = self._CutPlane(parent=self)
self._cutPlane.setVisible(False)
self._isogroup = primitives.GroupDepthOffset()
@@ -405,6 +472,26 @@ class ScalarField3D(BaseNodeItem):
self._cutPlane._getScenePrimitive(),
self._isogroup]
+ @staticmethod
+ def _computeRangeFromData(data):
+ """Compute range info (min, min positive, max) from data
+
+ :param Union[numpy.ndarray,None] data:
+ :return: Union[List[float],None]
+ """
+ if data is None:
+ return None
+
+ dataRange = min_max(data, min_positive=True, finite=True)
+ if dataRange.minimum is None: # Only non-finite data
+ return None
+
+ if dataRange is not None:
+ min_positive = dataRange.min_positive
+ if min_positive is None:
+ min_positive = float('nan')
+ return dataRange.minimum, min_positive, dataRange.maximum
+
def setData(self, data, copy=True):
"""Set the 3D scalar data represented by this item.
@@ -418,7 +505,6 @@ class ScalarField3D(BaseNodeItem):
"""
if data is None:
self._data = None
- self._dataRange = None
self._boundedGroup.shape = None
else:
@@ -427,21 +513,9 @@ class ScalarField3D(BaseNodeItem):
assert min(data.shape) >= 2
self._data = data
-
- # Store data range info
- dataRange = min_max(self._data, min_positive=True, finite=True)
- if dataRange.minimum is None: # Only non-finite data
- dataRange = None
-
- if dataRange is not None:
- min_positive = dataRange.min_positive
- if min_positive is None:
- min_positive = float('nan')
- dataRange = dataRange.minimum, min_positive, dataRange.maximum
- self._dataRange = dataRange
-
self._boundedGroup.shape = self._data.shape
+ self._dataRange = self._computeRangeFromData(self._data)
self._updated(ItemChangedType.DATA)
def getData(self, copy=True):
@@ -506,7 +580,7 @@ class ScalarField3D(BaseNodeItem):
:return: isosurface object
:rtype: ~silx.gui.plot3d.items.volume.Isosurface
"""
- isosurface = Isosurface(parent=self)
+ isosurface = self._Isosurface(parent=self)
isosurface.setColor(color)
if callable(level):
isosurface.setAutoLevelFunction(level)
@@ -561,8 +635,164 @@ class ScalarField3D(BaseNodeItem):
# BaseNodeItem
def getItems(self):
- """Returns the list of items currently present in the ScalarField3D.
+ """Returns the list of items currently present in this item.
:rtype: tuple
"""
return self.getCutPlanes() + self.getIsosurfaces()
+
+
+##################
+# ComplexField3D #
+##################
+
+class ComplexCutPlane(CutPlane, ComplexMixIn):
+ """Class representing a cutting plane in a :class:`ComplexField3D` item.
+
+ :param parent: 3D Data set in which the cut plane is applied.
+ """
+
+ def __init__(self, parent):
+ ComplexMixIn.__init__(self)
+ CutPlane.__init__(self, parent=parent)
+
+ def _syncDataWithParent(self):
+ """Synchronize this instance data with that of its parent"""
+ parent = self.parent()
+ if parent is None:
+ data, range_ = None, None
+ else:
+ mode = self.getComplexMode()
+ data = parent.getData(mode=mode, copy=False)
+ range_ = parent.getDataRange(mode=mode)
+ self._updateData(data, range_)
+
+ def _updated(self, event=None):
+ """Handle update of the cut plane (and take care of mode change
+
+ :param Union[None,ItemChangedType] event: The kind of update
+ """
+ if event == ItemChangedType.COMPLEX_MODE:
+ self._syncDataWithParent()
+ super(ComplexCutPlane, self)._updated(event)
+
+
+class ComplexIsosurface(Isosurface):
+ """Class representing an iso-surface in a :class:`ComplexField3D` item.
+
+ :param parent: The DataItem3D this iso-surface belongs to
+ """
+
+ def __init__(self, parent):
+ super(ComplexIsosurface, self).__init__(parent)
+
+ def _syncDataWithParent(self):
+ """Synchronize this instance data with that of its parent"""
+ parent = self.parent()
+ if parent is None:
+ self._data = None
+ else:
+ self._data = parent.getData(
+ mode=parent.getComplexMode(), copy=False)
+ self._updateScenePrimitive()
+
+ def _parentChanged(self, event):
+ """Handle data change in the parent this isosurface belongs to"""
+ if event == ItemChangedType.COMPLEX_MODE:
+ self._syncDataWithParent()
+ super(ComplexIsosurface, self)._parentChanged(event)
+
+
+class ComplexField3D(ScalarField3D, ComplexMixIn):
+ """3D complex field on a regular grid.
+
+ :param parent: The View widget this item belongs to.
+ """
+
+ _CutPlane = ComplexCutPlane
+ _Isosurface = ComplexIsosurface
+
+ def __init__(self, parent=None):
+ self._dataRangeCache = None
+
+ ComplexMixIn.__init__(self)
+ ScalarField3D.__init__(self, parent=parent)
+
+ @docstring(ComplexMixIn)
+ def setComplexMode(self, mode):
+ if mode != self.getComplexMode():
+ self.clearIsosurfaces() # Reset isosurfaces
+ ComplexMixIn.setComplexMode(self, mode)
+
+ def setData(self, data, copy=True):
+ """Set the 3D complex data represented by this item.
+
+ Dataset order is zyx (i.e., first dimension is z).
+
+ :param data: 3D array
+ :type data: 3D numpy.ndarray of float32 with shape at least (2, 2, 2)
+ :param bool copy:
+ True (default) to make a copy,
+ False to avoid copy (DO NOT MODIFY data afterwards)
+ """
+ if data is None:
+ self._data = None
+ self._dataRangeCache = None
+ self._boundedGroup.shape = None
+
+ else:
+ data = numpy.array(data, copy=copy, dtype=numpy.complex64, order='C')
+ assert data.ndim == 3
+ assert min(data.shape) >= 2
+
+ self._data = data
+ self._dataRangeCache = {}
+ self._boundedGroup.shape = self._data.shape
+
+ self._updated(ItemChangedType.DATA)
+
+ def getData(self, copy=True, mode=None):
+ """Return 3D dataset.
+
+ This method does not cache data converted to a specific mode,
+ it computes it for each request.
+
+ :param bool copy:
+ True (default) to get a copy,
+ False to get the internal data (DO NOT modify!)
+ :param Union[None,Mode] mode:
+ The kind of data to retrieve.
+ If None (the default), it returns the complex data,
+ else it computes the requested scalar data.
+ :return: The data set (or None if not set)
+ :rtype: Union[numpy.ndarray,None]
+ """
+ if mode is None:
+ return super(ComplexField3D, self).getData(copy=copy)
+ else:
+ return self._convertComplexData(self._data, mode)
+
+ def getDataRange(self, mode=None):
+ """Return the range of the requested data as a 3-tuple of values.
+
+ Positive min is NaN if no data is positive.
+
+ :param Union[None,Mode] mode:
+ The kind of data for which to get the range information.
+ If None (the default), it returns the data range for the current mode,
+ else it returns the data range for the requested mode.
+ :return: (min, positive min, max) or None.
+ :rtype: Union[None,List[float]]
+ """
+ if self._dataRangeCache is None:
+ return None
+
+ if mode is None:
+ mode = self.getComplexMode()
+
+ if mode not in self._dataRangeCache:
+ # Compute it and store it in cache
+ data = self.getData(copy=False, mode=mode)
+ self._dataRangeCache[mode] = self._computeRangeFromData(data)
+
+ return self._dataRangeCache[mode]