diff options
Diffstat (limited to 'silx/gui/plot/stats/stats.py')
-rw-r--r-- | silx/gui/plot/stats/stats.py | 497 |
1 files changed, 414 insertions, 83 deletions
diff --git a/silx/gui/plot/stats/stats.py b/silx/gui/plot/stats/stats.py index ad61536..755b185 100644 --- a/silx/gui/plot/stats/stats.py +++ b/silx/gui/plot/stats/stats.py @@ -1,7 +1,7 @@ # coding: utf-8 # /*########################################################################## # -# Copyright (c) 2017-2019 European Synchrotron Radiation Facility +# Copyright (c) 2017-2020 European Synchrotron Radiation Facility # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -22,7 +22,9 @@ # THE SOFTWARE. # # ###########################################################################*/ -"""This module provides the :class:`Scatter` item of the :class:`Plot`. +"""This module provides mechanism relative to stats calculation within a +:class:`PlotWidget`. +It also include the implementation of the statistics themselves. """ __authors__ = ["H. Payno"] @@ -31,13 +33,19 @@ __date__ = "06/06/2018" from collections import OrderedDict +from functools import lru_cache import logging import numpy +import numpy.ma from .. import items -from ....math.combo import min_max +from ..CurvesROIWidget import ROI +from ..items.roi import RegionOfInterest +from ....math.combo import min_max +from silx.utils.proxy import docstring +from ....utils.deprecation import deprecated logger = logging.getLogger(__name__) @@ -60,7 +68,8 @@ class Stats(OrderedDict): for stat in _statslist: self.add(stat) - def calculate(self, item, plot, onlimits): + def calculate(self, item, plot, onlimits, roi, data_changed=False, + roi_changed=False): """ Call all :class:`Stat` object registered and return the result of the computation. @@ -69,38 +78,31 @@ class Stats(OrderedDict): :param plot: plot containing the item :param bool onlimits: True if we want to apply statistic only on visible data. + :param roi: region of interest for statistic calculation. Incompatible + with the `onlimits` option. + :type roi: Union[None, :class:`~_RegionOfInterestBase`] + :param bool data_changed: did the data changed since last calculation. + :param bool roi_changed: did the associated roi (if any) has changed + since last calculation. :return dict: dictionary with :class:`Stat` name as ket and result of the calculation as value """ - context = None - # Check for PlotWidget items - if isinstance(item, items.Curve): - context = _CurveContext(item, plot, onlimits) - elif isinstance(item, items.ImageData): - context = _ImageContext(item, plot, onlimits) - elif isinstance(item, items.Scatter): - context = _ScatterContext(item, plot, onlimits) - elif isinstance(item, items.Histogram): - context = _HistogramContext(item, plot, onlimits) - else: - # Check for SceneWidget items - from ...plot3d import items as items3d # Lazy import - - if isinstance(item, (items3d.Scatter2D, items3d.Scatter3D)): - context = _plot3DScatterContext(item, plot, onlimits) - elif isinstance(item, (items3d.ImageData, items3d.ScalarField3D)): - context = _plot3DArrayContext(item, plot, onlimits) - - if context is None: - raise ValueError('Item type not managed') - res = {} + context = self._getContext(item=item, plot=plot, onlimits=onlimits, + roi=roi) for statName, stat in list(self.items()): if context.kind not in stat.compatibleKinds: logger.debug('kind %s not managed by statistic %s' % (context.kind, stat.name)) res[statName] = None else: + if roi_changed is True: + context.clear_mask() + if data_changed is True or roi_changed is True: + # if data changed or mask changed + context.clipData(item=item, plot=plot, onlimits=onlimits, + roi=roi) + # init roi and data res[statName] = stat.calculate(context) return res @@ -109,8 +111,40 @@ class Stats(OrderedDict): OrderedDict.__setitem__(self, key, value) def add(self, stat): + """Add a :class:`Stat` to the set + + :param Stat stat: stat to add to the set + """ self.__setitem__(key=stat.name, value=stat) + @staticmethod + @lru_cache(maxsize=50) + def _getContext(item, plot, onlimits, roi): + context = None + # Check for PlotWidget items + if isinstance(item, items.Curve): + context = _CurveContext(item, plot, onlimits, roi=roi) + elif isinstance(item, items.ImageData): + context = _ImageContext(item, plot, onlimits, roi=roi) + elif isinstance(item, items.Scatter): + context = _ScatterContext(item, plot, onlimits, roi=roi) + elif isinstance(item, items.Histogram): + context = _HistogramContext(item, plot, onlimits, roi=roi) + else: + # Check for SceneWidget items + from ...plot3d import items as items3d # Lazy import + + if isinstance(item, (items3d.Scatter2D, items3d.Scatter3D)): + context = _plot3DScatterContext(item, plot, onlimits, + roi=roi) + elif isinstance(item, + (items3d.ImageData, items3d.ScalarField3D)): + context = _plot3DArrayContext(item, plot, onlimits, + roi=roi) + if context is None: + raise ValueError('Item type not managed') + return context + class _StatsContext(object): """ @@ -127,8 +161,11 @@ class _StatsContext(object): :param plot: the plot containing the item :param bool onlimits: True if we want to apply statistic only on visible data. + :param roi: Region of interest for computing the statistics. + For now, incompatible with `onlimits` calculation + :type roi: Union[None,:class:`_RegionOfInterestBase`] """ - def __init__(self, item, kind, plot, onlimits): + def __init__(self, item, kind, plot, onlimits, roi): assert item assert plot assert type(onlimits) is bool @@ -136,9 +173,12 @@ class _StatsContext(object): self.min = None self.max = None self.data = None + self.roi = None + self.onlimits = onlimits self.values = None - """The array of data""" + """The array of data with limit filtering if any. Is a numpy.ma.array, + meaning that it embed the mask applied by the roi if any""" self.axes = None """A list of array of position on each axis. @@ -151,11 +191,69 @@ class _StatsContext(object): and the order is (x, y, z). """ - self.createContext(item, plot, onlimits) + self.clipData(item, plot, onlimits, roi=roi) + + def clipData(self, item, plot, onlimits, roi): + """ + Clip the data to the current mask to have accurate statistics + + :param item: item for whiwh we want to clip data + :param plot: plot containing the item + :param onlimits: do we want to apply statistic only on + visible data. + :param roi: Region of interest for computing the statistics. + :type roi: Union[None,:class:`_RegionOfInterestBase`] + """ + raise NotImplementedError() - def createContext(self, item, plot, onlimits): + def clear_mask(self): + """ + Remove the mask to force recomputation of it on next iteration + :return: + """ + raise NotImplementedError() + + @property + def mask(self): + if self.values is not None: + assert isinstance(self.values, numpy.ma.MaskedArray) + return self.values.mask + else: + return None + + @property + def is_mask_valid(self, **kwargs): + """Return if the mask is valid for the data or need to be recomputed""" + raise NotImplementedError("Base class") + + def _set_mask_validity(self, **kwargs): + """User to set some values that allows to define the mask properties + and boundaries""" raise NotImplementedError("Base class") + def clipData(self, item, plot, onlimits, roi): + """ + Function called before computing each statistics associated to this + context. It will insure the context for the (item, plot, onlimits, roi) + is created. + + :param item: item for which we want statistics + :param plot: plot containing the statistics + :param bool onlimits: True if we want to apply statistic only on + visible data. + :param roi: Region of interest for computing the statistics. + For now, incompatible with `onlimits` calculation + :type roi: Union[None,:class:`_RegionOfInterestBase`] + """ + raise NotImplementedError("Base class") + + @deprecated(reason="context are now stored and keep during stats life." + "So this function will be called only once", + replacement="clipData", since_version="0.13.0") + def createContext(self, item, plot, onlimits, roi): + return self.clipData(item=item, plot=plot, onlimits=onlimits, + roi=roi) + def isStructuredData(self): """Returns True if data as an array-like structure. @@ -184,8 +282,34 @@ class _StatsContext(object): else: return self.values.ndim == 1 + def _checkContextInputs(self, item, plot, onlimits, roi): + if roi is not None and onlimits is True: + raise ValueError('Stats context is unable to manage both a ROI' + 'and the `onlimits` option') + + +class _ScatterCurveHistoMixInContext(_StatsContext): + def __init__(self, kind, item, plot, onlimits, roi): + self.clear_mask() + _StatsContext.__init__(self, item=item, kind=kind, + plot=plot, onlimits=onlimits, roi=roi) -class _CurveContext(_StatsContext): + def _set_mask_validity(self, onlimits, from_, to_): + self._onlimits = onlimits + self._from_ = from_ + self._to_ = to_ + + def clear_mask(self): + self._onlimits = None + self._from_ = None + self._to_ = None + + def is_mask_valid(self, onlimits, from_, to_): + return (onlimits == self.onlimits and from_ == self._from_ and + to_ == self._to_) + + +class _CurveContext(_ScatterCurveHistoMixInContext): """ StatsContext for :class:`Curve` @@ -193,32 +317,63 @@ class _CurveContext(_StatsContext): :param plot: the plot containing the item :param bool onlimits: True if we want to apply statistic only on visible data. + :param roi: Region of interest for computing the statistics. + For now, incompatible with `onlinits` calculation + :type roi: Union[None, :class:`ROI`] """ - def __init__(self, item, plot, onlimits): - _StatsContext.__init__(self, kind='curve', item=item, - plot=plot, onlimits=onlimits) - - def createContext(self, item, plot, onlimits): + def __init__(self, item, plot, onlimits, roi): + _ScatterCurveHistoMixInContext.__init__(self, kind='curve', item=item, + plot=plot, onlimits=onlimits, + roi=roi) + + @docstring(_StatsContext) + def clipData(self, item, plot, onlimits, roi): + self._checkContextInputs(item=item, plot=plot, onlimits=onlimits, + roi=roi) + self.roi = roi + self.onlimits = onlimits xData, yData = item.getData(copy=True)[0:2] if onlimits: minX, maxX = plot.getXAxis().getLimits() - mask = (minX <= xData) & (xData <= maxX) + if self.is_mask_valid(onlimits=onlimits, from_=minX, to_=maxX): + mask = self.mask + else: + mask = (minX <= xData) & (xData <= maxX) yData = yData[mask] xData = xData[mask] + mask = numpy.zeros_like(yData) + elif roi: + minX, maxX = roi.getFrom(), roi.getTo() + if self.is_mask_valid(onlimits=onlimits, from_=minX, to_=maxX): + mask = self.mask + else: + mask = (minX <= xData) & (xData <= maxX) + mask = mask == 0 + mask = mask.astype(numpy.int32) + else: + mask = numpy.zeros_like(yData) self.xData = xData self.yData = yData - if len(yData) > 0: - self.min, self.max = min_max(yData) + self.values = numpy.ma.array(yData, mask=mask) + unmasked_data = self.values.compressed() + if len(unmasked_data) > 0: + self.min, self.max = min_max(unmasked_data) else: self.min, self.max = None, None self.data = (xData, yData) - self.values = yData + self.axes = (xData,) + def _checkContextInputs(self, item, plot, onlimits, roi): + _StatsContext._checkContextInputs(self, item=item, plot=plot, + onlimits=onlimits, roi=roi) + if roi is not None and not isinstance(roi, ROI): + raise TypeError('curve `context` can ony manage 1D roi') -class _HistogramContext(_StatsContext): + +class _HistogramContext(_ScatterCurveHistoMixInContext): """ StatsContext for :class:`Histogram` @@ -226,32 +381,66 @@ class _HistogramContext(_StatsContext): :param plot: the plot containing the item :param bool onlimits: True if we want to apply statistic only on visible data. + :param roi: Region of interest for computing the statistics. + For now, incompatible with `onlinits` calculation + :type roi: Union[None, :class:`ROI`] """ - def __init__(self, item, plot, onlimits): - _StatsContext.__init__(self, kind='histogram', item=item, - plot=plot, onlimits=onlimits) - - def createContext(self, item, plot, onlimits): + def __init__(self, item, plot, onlimits, roi): + _ScatterCurveHistoMixInContext.__init__(self, kind='histogram', + item=item, plot=plot, + onlimits=onlimits, roi=roi) + + @docstring(_StatsContext) + def clipData(self, item, plot, onlimits, roi): + self._checkContextInputs(item=item, plot=plot, onlimits=onlimits, + roi=roi) yData, edges = item.getData(copy=True)[0:2] xData = item._revertComputeEdges(x=edges, histogramType=item.getAlignment()) + if onlimits: minX, maxX = plot.getXAxis().getLimits() - mask = (minX <= xData) & (xData <= maxX) + if self.is_mask_valid(onlimits, from_=minX, to_=maxX): + mask = self.mask + else: + mask = (minX <= xData) & (xData <= maxX) + self._set_mask_validity(onlimits=True, from_=minX, to_=maxX) + elif roi: + if self.is_mask_valid(onlimits, from_=roi._fromdata, to_=roi._todata): + mask = self.mask + else: + mask = (roi._fromdata <= xData) & (xData <= roi._todata) + mask = mask == 0 + self._set_mask_validity(onlimits=True, from_=roi._fromdata, + to_=roi._todata) + else: + mask = numpy.zeros_like(self.data) + + if onlimits: yData = yData[mask] xData = xData[mask] + self.data = (xData, yData) + self.values = numpy.ma.array(yData, mask=mask) + self.axes = (xData,) + self.xData = xData self.yData = yData - if len(yData) > 0: - self.min, self.max = min_max(yData) + + unmasked_data = self.values.compressed() + if len(unmasked_data) > 0: + self.min, self.max = min_max(unmasked_data) else: self.min, self.max = None, None - self.data = (xData, yData) - self.values = yData - self.axes = (xData,) + def _checkContextInputs(self, item, plot, onlimits, roi): + _StatsContext._checkContextInputs(self, item=item, plot=plot, + onlimits=onlimits, roi=roi) + + if roi is not None and not isinstance(roi, ROI): + raise TypeError('curve `context` can ony manage 1D roi') -class _ScatterContext(_StatsContext): + +class _ScatterContext(_ScatterCurveHistoMixInContext): """StatsContext scatter plots. It supports :class:`~silx.gui.plot.items.Scatter`. @@ -260,12 +449,19 @@ class _ScatterContext(_StatsContext): :param plot: the plot containing the item :param bool onlimits: True if we want to apply statistic only on visible data. + :param roi: Region of interest for computing the statistics. + For now, incompatible with `onlinits` calculation + :type roi: Union[None, :class:`ROI`] """ - def __init__(self, item, plot, onlimits): - _StatsContext.__init__(self, kind='scatter', item=item, plot=plot, - onlimits=onlimits) - - def createContext(self, item, plot, onlimits): + def __init__(self, item, plot, onlimits, roi): + _ScatterCurveHistoMixInContext.__init__(self, kind='scatter', + item=item, plot=plot, + onlimits=onlimits, roi=roi) + + @docstring(_ScatterCurveHistoMixInContext) + def clipData(self, item, plot, onlimits, roi): + self._checkContextInputs(item=item, plot=plot, onlimits=onlimits, + roi=roi) valueData = item.getValueData(copy=True) xData = item.getXData(copy=True) yData = item.getYData(copy=True) @@ -283,34 +479,89 @@ class _ScatterContext(_StatsContext): xData = xData[(minY <= yData) & (yData <= maxY)] yData = yData[(minY <= yData) & (yData <= maxY)] - if len(valueData) > 0: - self.min, self.max = min_max(valueData) + if roi: + if self.is_mask_valid(onlimits=onlimits, from_=roi.getFrom(), + to_=roi.getTo()): + mask = self.mask + else: + mask = (xData < roi.getFrom()) | (xData > roi.getTo()) else: - self.min, self.max = None, None + mask = numpy.zeros_like(xData) + self.data = (xData, yData, valueData) - self.values = valueData + self.values = numpy.ma.array(valueData, mask=mask) self.axes = (xData, yData) + unmasked_values = self.values.compressed() + if len(unmasked_values) > 0: + self.min, self.max = min_max(unmasked_values) + else: + self.min, self.max = None, None + + def _checkContextInputs(self, item, plot, onlimits, roi): + _StatsContext._checkContextInputs(self, item=item, plot=plot, + onlimits=onlimits, roi=roi) + + if roi is not None and not isinstance(roi, ROI): + raise TypeError('curve `context` can ony manage 1D roi') + class _ImageContext(_StatsContext): """StatsContext for images. It supports :class:`~silx.gui.plot.items.ImageData`. + :warning: behaviour of scale images: now the statistics are computed on + the entire data array (there is no sampling in the array or + interpolation regarding the scale). + This also mean that the result can differ from what is displayed. + But I guess there is no perfect behaviour. + + :warning: `isIn` functions for image context: for now have basically a + binary approach, the pixel is in a roi or not. To have a fully + 'correct behaviour' we should add a weight on stats calculation + to moderate the pixel value. + :param item: the item for which we want to compute the context :param plot: the plot containing the item :param bool onlimits: True if we want to apply statistic only on visible data. + :param roi: Region of interest for computing the statistics. + For now, incompatible with `onlinits` calculation + :type roi: Union[None, :class:`ROI`] """ - def __init__(self, item, plot, onlimits): + def __init__(self, item, plot, onlimits, roi): + self.clear_mask() _StatsContext.__init__(self, kind='image', item=item, - plot=plot, onlimits=onlimits) - - def createContext(self, item, plot, onlimits): + plot=plot, onlimits=onlimits, roi=roi) + + def _set_mask_validity(self, xmin: float, xmax: float, ymin: float, ymax + : float): + self._mask_x_min = xmin + self._mask_x_max = xmax + self._mask_y_min = ymin + self._mask_y_max = ymax + + def clear_mask(self): + self._mask_x_min = None + self._mask_x_max = None + self._mask_y_min = None + self._mask_y_max = None + + def is_mask_valid(self, xmin, xmax, ymin, ymax): + return (xmin == self._mask_x_min and xmax == self._mask_x_max and + ymin == self._mask_y_min and ymax == self._mask_y_max) + + @docstring(_StatsContext) + def clipData(self, item, plot, onlimits, roi): + self._checkContextInputs(item=item, plot=plot, onlimits=onlimits, + roi=roi) self.origin = item.getOrigin() self.scale = item.getScale() self.data = item.getData(copy=True) + mask = numpy.zeros_like(self.data) + """mask use to know of the stat should be count in or not""" if onlimits: minX, maxX = plot.getXAxis().getLimits() @@ -324,21 +575,50 @@ class _ImageContext(_StatsContext): XMinBound = max(XMinBound, 0) YMinBound = max(YMinBound, 0) + if onlimits: if XMaxBound <= XMinBound or YMaxBound <= YMinBound: self.data = None else: self.data = self.data[YMinBound:YMaxBound + 1, XMinBound:XMaxBound + 1] - if self.data.size > 0: - self.min, self.max = min_max(self.data) + mask = numpy.zeros_like(self.data) + elif roi: + minX, maxX = 0, self.data.shape[1] + minY, maxY = 0, self.data.shape[0] + + XMinBound = max(minX, 0) + YMinBound = max(minY, 0) + XMaxBound = min(maxX, self.data.shape[1]) + YMaxBound = min(maxY, self.data.shape[0]) + + if self.is_mask_valid(xmin=XMinBound, xmax=XMaxBound, + ymin=YMinBound, ymax=YMaxBound): + mask = self.mask + else: + for x in range(XMinBound, XMaxBound): + for y in range(YMinBound, YMaxBound): + _x = (x * self.scale[0]) + self.origin[0] + _y = (y * self.scale[1]) + self.origin[1] + mask[y, x] = not roi.contains((_x, _y)) + self._set_mask_validity(xmin=XMinBound, xmax=XMaxBound, + ymin=YMinBound, ymax=YMaxBound) + self.values = numpy.ma.array(self.data, mask=mask) + if self.values.compressed().size > 0: + self.min, self.max = min_max(self.values.compressed()) else: self.min, self.max = None, None - self.values = self.data if self.values is not None: self.axes = (self.origin[1] + self.scale[1] * numpy.arange(self.data.shape[0]), self.origin[0] + self.scale[0] * numpy.arange(self.data.shape[1])) + def _checkContextInputs(self, item, plot, onlimits, roi): + _StatsContext._checkContextInputs(self, item=item, plot=plot, + onlimits=onlimits, roi=roi) + + if roi is not None and not isinstance(roi, RegionOfInterest): + raise TypeError('curve `context` can ony manage 2D roi') + class _plot3DScatterContext(_StatsContext): """StatsContext for 3D scatter plots. @@ -350,16 +630,26 @@ class _plot3DScatterContext(_StatsContext): :param plot: the plot containing the item :param bool onlimits: True if we want to apply statistic only on visible data. + :param roi: Region of interest for computing the statistics. + For now, incompatible with `onlinits` calculation + :type roi: Union[None, :class:`ROI`] """ - def __init__(self, item, plot, onlimits): + def __init__(self, item, plot, onlimits, roi): _StatsContext.__init__(self, kind='scatter', item=item, plot=plot, - onlimits=onlimits) + onlimits=onlimits, roi=roi) - def createContext(self, item, plot, onlimits): + @docstring(_StatsContext) + def clipData(self, item, plot, onlimits, roi): + self._checkContextInputs(item=item, plot=plot, onlimits=onlimits, + roi=roi) if onlimits: raise RuntimeError("Unsupported plot %s" % str(plot)) - values = item.getValueData(copy=False) + if roi: + logger.warning("Roi are unsupported on volume for now") + mask = numpy.zeros_like(values) + else: + mask = numpy.zeros_like(values) if values is not None and len(values) > 0: self.values = values @@ -367,13 +657,20 @@ class _plot3DScatterContext(_StatsContext): if self.values.ndim == 3: axes.append(item.getZData(copy=False)) self.axes = tuple(axes) - self.min, self.max = min_max(self.values) + self.values = numpy.ma.array(self.values, mask=mask) else: self.values = None self.axes = None self.min, self.max = None, None + def _checkContextInputs(self, item, plot, onlimits, roi): + _StatsContext._checkContextInputs(self, item=item, plot=plot, + onlimits=onlimits, roi=roi) + + if roi is not None and not isinstance(roi, RegionOfInterest): + raise TypeError('curve `context` can ony manage 2D roi') + class _plot3DArrayContext(_StatsContext): """StatsContext for 3D scalar field and data image. @@ -385,26 +682,45 @@ class _plot3DArrayContext(_StatsContext): :param plot: the plot containing the item :param bool onlimits: True if we want to apply statistic only on visible data. + :param roi: Region of interest for computing the statistics. + For now, incompatible with `onlinits` calculation + :type roi: Union[None, :class:`ROI`] """ - def __init__(self, item, plot, onlimits): + def __init__(self, item, plot, onlimits, roi): _StatsContext.__init__(self, kind='image', item=item, plot=plot, - onlimits=onlimits) + onlimits=onlimits, roi=roi) - def createContext(self, item, plot, onlimits): + @docstring(_StatsContext) + def clipData(self, item, plot, onlimits, roi): + self._checkContextInputs(item=item, plot=plot, onlimits=onlimits, + roi=roi) if onlimits: raise RuntimeError("Unsupported plot %s" % str(plot)) values = item.getData(copy=False) + if roi: + logger.warning("Roi are unsuported on volume for now") + mask = numpy.zeros_like(values) + else: + mask = numpy.zeros_like(values) if values is not None and len(values) > 0: self.values = values self.axes = tuple([numpy.arange(size) for size in self.values.shape]) self.min, self.max = min_max(self.values) + self.values = numpy.ma.array(self.values, mask=mask) else: self.values = None self.axes = None self.min, self.max = None, None + def _checkContextInputs(self, item, plot, onlimits, roi): + _StatsContext._checkContextInputs(self, item=item, plot=plot, + onlimits=onlimits, roi=roi) + + if roi is not None and not isinstance(roi, RegionOfInterest): + raise TypeError('curve `context` can ony manage 2D roi') + BASIC_COMPATIBLE_KINDS = 'curve', 'image', 'scatter', 'histogram' @@ -456,6 +772,7 @@ class Stat(StatBase): StatBase.__init__(self, name, kinds) self._fct = fct + @docstring(StatBase) def calculate(self, context): if context.values is not None: if context.kind in self.compatibleKinds: @@ -472,6 +789,7 @@ class StatMin(StatBase): def __init__(self): StatBase.__init__(self, name='min') + @docstring(StatBase) def calculate(self, context): return context.min @@ -481,6 +799,7 @@ class StatMax(StatBase): def __init__(self): StatBase.__init__(self, name='max') + @docstring(StatBase) def calculate(self, context): return context.max @@ -490,6 +809,7 @@ class StatDelta(StatBase): def __init__(self): StatBase.__init__(self, name='delta') + @docstring(StatBase) def calculate(self, context): return context.max - context.min @@ -506,14 +826,17 @@ class _StatCoord(StatBase): :param int index: Index in the flattened data array :rtype: List[int] """ - if context.isStructuredData(): + + axes = context.axes + + if context.isStructuredData() or context.roi: coordinates = [] - for axis in reversed(context.axes): + for axis in reversed(axes): coordinates.append(axis[index % len(axis)]) index = index // len(axis) return tuple(coordinates) else: - return tuple(axis[index] for axis in context.axes) + return tuple(axis[index] for axis in axes) class StatCoordMin(_StatCoord): @@ -521,13 +844,15 @@ class StatCoordMin(_StatCoord): def __init__(self): _StatCoord.__init__(self, name='coords min') + @docstring(StatBase) def calculate(self, context): if context.values is None or not context.isScalarData(): return None - index = numpy.argmin(context.values) + index = context.values.argmin() return self._indexToCoordinates(context, index) + @docstring(StatBase) def getToolTip(self, kind): return "Coordinates of the first minimum value of the data" @@ -537,13 +862,17 @@ class StatCoordMax(_StatCoord): def __init__(self): _StatCoord.__init__(self, name='coords max') + @docstring(StatBase) def calculate(self, context): if context.values is None or not context.isScalarData(): return None - index = numpy.argmax(context.values) + # TODO: the values should be a mask array by default, will be simpler + # if possible + index = context.values.argmax() return self._indexToCoordinates(context, index) + @docstring(StatBase) def getToolTip(self, kind): return "Coordinates of the first maximum value of the data" @@ -553,11 +882,12 @@ class StatCOM(StatBase): def __init__(self): StatBase.__init__(self, name='COM', description='Center of mass') + @docstring(StatBase) def calculate(self, context): if context.values is None or not context.isScalarData(): return None - values = numpy.array(context.values, dtype=numpy.float64) + values = numpy.ma.array(context.values, mask=context.mask, dtype=numpy.float64) sum_ = numpy.sum(values) if sum_ == 0.: return (numpy.nan,) * len(context.axes) @@ -573,5 +903,6 @@ class StatCOM(StatBase): return tuple( numpy.sum(axis * values) / sum_ for axis in context.axes) + @docstring(StatBase) def getToolTip(self, kind): return "Compute the center of mass of the dataset" |