From 5eef97a6e3b66cd1341f81b9299af5fb7cece04f Mon Sep 17 00:00:00 2001 From: Valentin Valls Date: Wed, 18 May 2022 16:53:27 +0200 Subject: [PATCH 1/3] Dispatch plot_state_model into bliss.flint.filters modules --- bliss/flint/filters/__init__.py | 0 bliss/flint/filters/derivative.py | 131 +++ bliss/flint/filters/gaussian_fit.py | 79 ++ bliss/flint/filters/max.py | 99 ++ bliss/flint/filters/min.py | 99 ++ bliss/flint/filters/negative.py | 99 ++ bliss/flint/filters/normalized.py | 162 ++++ bliss/flint/filters/normalized_zero_one.py | 159 ++++ bliss/flint/flint_api.py | 5 +- bliss/flint/helper/style_helper.py | 7 +- bliss/flint/model/plot_item_model.py | 173 ++++ bliss/flint/model/plot_state_model.py | 867 +----------------- bliss/flint/widgets/_property_tree_helper.py | 5 +- bliss/flint/widgets/curve_plot.py | 9 +- bliss/flint/widgets/curve_plot_property.py | 30 +- bliss/flint/widgets/one_dim_plot_property.py | 3 +- bliss/flint/widgets/utils/plot_helper.py | 4 +- tests/qt/flint/helper/test_model_helper.py | 18 +- tests/qt/flint/model/test_persistence.py | 22 +- tests/qt/flint/model/test_plot_item_model.py | 7 +- tests/qt/flint/model/test_plot_state_model.py | 35 +- 21 files changed, 1091 insertions(+), 922 deletions(-) create mode 100644 bliss/flint/filters/__init__.py create mode 100644 bliss/flint/filters/derivative.py create mode 100644 bliss/flint/filters/gaussian_fit.py create mode 100644 bliss/flint/filters/max.py create mode 100644 bliss/flint/filters/min.py create mode 100644 bliss/flint/filters/negative.py create mode 100644 bliss/flint/filters/normalized.py create mode 100644 bliss/flint/filters/normalized_zero_one.py diff --git a/bliss/flint/filters/__init__.py b/bliss/flint/filters/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/bliss/flint/filters/derivative.py b/bliss/flint/filters/derivative.py new file mode 100644 index 0000000000..985cb5a459 --- /dev/null +++ b/bliss/flint/filters/derivative.py @@ -0,0 +1,131 @@ +# -*- coding: utf-8 -*- +# +# This file is part of the bliss project +# +# Copyright (c) 2015-2022 Beamline Control Unit, ESRF +# Distributed under the GNU LGPLv3. See LICENSE for more info. + +""" +Implementation of a derivative filter. +""" +from __future__ import annotations +from typing import Optional +from typing import NamedTuple +from typing import Dict +from typing import Any + +import numpy +import logging + +from ..model import scan_model +from ..model import plot_model +from ..model import plot_item_model +from ..model.plot_item_model import ComputedCurveItem +from ..utils import mathutils + +_logger = logging.getLogger(__name__) + + +class DerivativeData(NamedTuple): + xx: numpy.ndarray + yy: numpy.ndarray + nb_points: int + + +class DerivativeItem(ComputedCurveItem, plot_model.IncrementalComputableMixIn): + """This item use the scan data to process result before displaying it.""" + + EXTRA_POINTS = 5 + """Extra points needed before and after a single point to compute a result""" + + def __init__(self, parent=None): + ComputedCurveItem.__init__(self, parent=parent) + plot_model.IncrementalComputableMixIn.__init__(self) + + def name(self) -> str: + return "Derivative" + + def __getstate__(self): + state: Dict[str, Any] = {} + state.update(plot_model.ChildItem.__getstate__(self)) + state.update(plot_item_model.CurveMixIn.__getstate__(self)) + return state + + def __setstate__(self, state): + plot_model.ChildItem.__setstate__(self, state) + plot_item_model.CurveMixIn.__setstate__(self, state) + + def compute(self, scan: scan_model.Scan) -> Optional[DerivativeData]: + sourceItem = self.source() + + xx = sourceItem.xArray(scan) + yy = sourceItem.yArray(scan) + if xx is None or yy is None: + return None + + try: + derived = mathutils.derivate(xx, yy) + except Exception as e: + _logger.debug("Error while computing derivative", exc_info=True) + result = DerivativeData(numpy.array([]), numpy.array([]), len(xx)) + raise plot_model.ComputeError( + "Error while creating derivative.\n" + str(e), result=result + ) + + return DerivativeData(derived[0], derived[1], len(xx)) + + def incrementalCompute( + self, previousResult: DerivativeData, scan: scan_model.Scan + ) -> DerivativeData: + """Compute a data using the previous value as basis + + The derivative function expect 5 extra points before and after the + points it can compute. + + The last computed point have to be recomputed. + + This code is deeply coupled with the implementation of the derivative + function. + """ + sourceItem = self.source() + xx = sourceItem.xArray(scan) + yy = sourceItem.yArray(scan) + if xx is None or yy is None: + raise ValueError("Non empty data expected") + + nb = previousResult.nb_points + if nb == len(xx): + # obviously nothing to compute + return previousResult + nextNb = len(xx) + + # The last point have to be recomputed + LAST = 1 + + if len(xx) <= 2 * self.EXTRA_POINTS + LAST: + return DerivativeData(numpy.array([]), numpy.array([]), nextNb) + + if len(previousResult.xx) == 0: + # If there is no previous point, there is no need to compute it + LAST = 0 + + xx = xx[nb - 2 * self.EXTRA_POINTS - LAST :] + yy = yy[nb - 2 * self.EXTRA_POINTS - LAST :] + + derived = mathutils.derivate(xx, yy) + + xx = numpy.append(previousResult.xx[:-1], derived[0]) + yy = numpy.append(previousResult.yy[:-1], derived[1]) + + result = DerivativeData(xx, yy, nextNb) + return result + + def displayName(self, axisName, scan: scan_model.Scan) -> str: + """Helper to reach the axis display name""" + sourceItem = self.source() + if axisName == "x": + return sourceItem.displayName("x", scan) + elif axisName == "y": + return "d(%s)" % sourceItem.displayName("y", scan) + else: + assert False diff --git a/bliss/flint/filters/gaussian_fit.py b/bliss/flint/filters/gaussian_fit.py new file mode 100644 index 0000000000..6c710cc96f --- /dev/null +++ b/bliss/flint/filters/gaussian_fit.py @@ -0,0 +1,79 @@ +# -*- coding: utf-8 -*- +# +# This file is part of the bliss project +# +# Copyright (c) 2015-2022 Beamline Control Unit, ESRF +# Distributed under the GNU LGPLv3. See LICENSE for more info. + +""" +Implementation of a gaussian fit filter. +""" +from __future__ import annotations +from typing import Optional +from typing import NamedTuple +from typing import Dict +from typing import Any + +import numpy +import logging + +from ..model import scan_model +from ..model import plot_model +from ..model import plot_item_model +from ..model.plot_item_model import ComputedCurveItem +from ..utils import mathutils + +_logger = logging.getLogger(__name__) + + +class GaussianFitData(NamedTuple): + xx: numpy.ndarray + yy: numpy.ndarray + fit: mathutils.GaussianFitResult + + +class GaussianFitItem(ComputedCurveItem, plot_model.ComputableMixIn): + """This item use the scan data to process result before displaying it.""" + + def __getstate__(self): + state: Dict[str, Any] = {} + state.update(plot_model.ChildItem.__getstate__(self)) + state.update(plot_item_model.CurveMixIn.__getstate__(self)) + return state + + def __setstate__(self, state): + plot_model.ChildItem.__setstate__(self, state) + plot_item_model.CurveMixIn.__setstate__(self, state) + + def compute(self, scan: scan_model.Scan) -> Optional[GaussianFitData]: + sourceItem = self.source() + + xx = sourceItem.xArray(scan) + yy = sourceItem.yArray(scan) + if xx is None or yy is None: + return None + + try: + fit = mathutils.fit_gaussian(xx, yy) + except Exception as e: + _logger.debug("Error while computing gaussian fit", exc_info=True) + result = GaussianFitData(numpy.array([]), numpy.array([]), None) + raise plot_model.ComputeError( + "Error while creating gaussian fit.\n" + str(e), result=result + ) + + yy = fit.transform(xx) + return GaussianFitData(xx, yy, fit) + + def name(self) -> str: + return "Gaussian" + + def displayName(self, axisName, scan: scan_model.Scan) -> str: + """Helper to reach the axis display name""" + sourceItem = self.source() + if axisName == "x": + return sourceItem.displayName("x", scan) + elif axisName == "y": + return "gaussian(%s)" % sourceItem.displayName("y", scan) + else: + assert False diff --git a/bliss/flint/filters/max.py b/bliss/flint/filters/max.py new file mode 100644 index 0000000000..d576462ea1 --- /dev/null +++ b/bliss/flint/filters/max.py @@ -0,0 +1,99 @@ +# -*- coding: utf-8 -*- +# +# This file is part of the bliss project +# +# Copyright (c) 2015-2022 Beamline Control Unit, ESRF +# Distributed under the GNU LGPLv3. See LICENSE for more info. + +""" +Implementation of a max filter. +""" +from __future__ import annotations +from typing import Optional +from typing import NamedTuple + +import numpy +import logging + +from ..model import scan_model +from ..model import plot_model +from ..model.plot_item_model import CurveStatisticItem + +_logger = logging.getLogger(__name__) + + +class MaxData(NamedTuple): + max_index: int + max_location_y: float + max_location_x: float + min_y_value: float + nb_points: int + + +class MaxCurveItem(CurveStatisticItem, plot_model.IncrementalComputableMixIn): + """Statistic identifying the maximum location of a curve.""" + + def name(self) -> str: + return "Max" + + def isResultValid(self, result): + return result is not None + + def compute(self, scan: scan_model.Scan) -> Optional[MaxData]: + sourceItem = self.source() + + xx = sourceItem.xArray(scan) + yy = sourceItem.yArray(scan) + if xx is None or yy is None: + return None + + max_index = numpy.argmax(yy) + min_y_value = numpy.min(yy) + max_location_x, max_location_y = xx[max_index], yy[max_index] + + result = MaxData( + max_index, max_location_y, max_location_x, min_y_value, len(xx) + ) + return result + + def incrementalCompute( + self, previousResult: MaxData, scan: scan_model.Scan + ) -> MaxData: + sourceItem = self.source() + + xx = sourceItem.xArray(scan) + yy = sourceItem.yArray(scan) + if xx is None or yy is None: + raise ValueError("Non empty data is expected") + + nb = previousResult.nb_points + if nb == len(xx): + # obviously nothing to compute + return previousResult + + xx = xx[nb:] + yy = yy[nb:] + + max_index = numpy.argmax(yy) + min_y_value = numpy.min(yy) + max_location_x, max_location_y = xx[max_index], yy[max_index] + max_index = max_index + nb + + if previousResult.min_y_value < min_y_value: + min_y_value = previousResult.min_y_value + + if previousResult.max_location_y > max_location_y: + # Update and return the previous result + return MaxData( + previousResult.max_index, + previousResult.max_location_y, + previousResult.max_location_x, + min_y_value, + nb + len(xx), + ) + + # Update and new return the previous result + result = MaxData( + max_index, max_location_y, max_location_x, min_y_value, nb + len(xx) + ) + return result diff --git a/bliss/flint/filters/min.py b/bliss/flint/filters/min.py new file mode 100644 index 0000000000..15643239b1 --- /dev/null +++ b/bliss/flint/filters/min.py @@ -0,0 +1,99 @@ +# -*- coding: utf-8 -*- +# +# This file is part of the bliss project +# +# Copyright (c) 2015-2022 Beamline Control Unit, ESRF +# Distributed under the GNU LGPLv3. See LICENSE for more info. + +""" +Implementation of a min filter. +""" +from __future__ import annotations +from typing import Optional +from typing import NamedTuple + +import numpy +import logging + +from ..model import scan_model +from ..model import plot_model +from ..model.plot_item_model import CurveStatisticItem + +_logger = logging.getLogger(__name__) + + +class MinData(NamedTuple): + min_index: int + min_location_y: float + min_location_x: float + max_y_value: float + nb_points: int + + +class MinCurveItem(CurveStatisticItem, plot_model.IncrementalComputableMixIn): + """Statistic identifying the minimum location of a curve.""" + + def name(self) -> str: + return "Min" + + def isResultValid(self, result): + return result is not None + + def compute(self, scan: scan_model.Scan) -> Optional[MinData]: + sourceItem = self.source() + + xx = sourceItem.xArray(scan) + yy = sourceItem.yArray(scan) + if xx is None or yy is None: + return None + + min_index = numpy.argmin(yy) + max_y_value = numpy.max(yy) + min_location_x, min_location_y = xx[min_index], yy[min_index] + + result = MinData( + min_index, min_location_y, min_location_x, max_y_value, len(xx) + ) + return result + + def incrementalCompute( + self, previousResult: MinData, scan: scan_model.Scan + ) -> MinData: + sourceItem = self.source() + + xx = sourceItem.xArray(scan) + yy = sourceItem.yArray(scan) + if xx is None or yy is None: + raise ValueError("Non empty data is expected") + + nb = previousResult.nb_points + if nb == len(xx): + # obviously nothing to compute + return previousResult + + xx = xx[nb:] + yy = yy[nb:] + + min_index = numpy.argmin(yy) + max_y_value = numpy.max(yy) + min_location_x, min_location_y = xx[min_index], yy[min_index] + min_index = min_index + nb + + if previousResult.max_y_value < max_y_value: + max_y_value = previousResult.max_y_value + + if previousResult.min_location_y < min_location_y: + # Update and return the previous result + return MinData( + previousResult.min_index, + previousResult.min_location_y, + previousResult.min_location_x, + max_y_value, + nb + len(xx), + ) + + # Update and new return the previous result + result = MinData( + min_index, min_location_y, min_location_x, max_y_value, nb + len(xx) + ) + return result diff --git a/bliss/flint/filters/negative.py b/bliss/flint/filters/negative.py new file mode 100644 index 0000000000..6547cb5fb7 --- /dev/null +++ b/bliss/flint/filters/negative.py @@ -0,0 +1,99 @@ +# -*- coding: utf-8 -*- +# +# This file is part of the bliss project +# +# Copyright (c) 2015-2022 Beamline Control Unit, ESRF +# Distributed under the GNU LGPLv3. See LICENSE for more info. + +""" +Implementation of a negative filter +""" +from __future__ import annotations +from typing import Optional +from typing import NamedTuple +from typing import Dict +from typing import Any + +import numpy +import logging + +from ..model import scan_model +from ..model import plot_model +from ..model import plot_item_model +from ..model.plot_item_model import ComputedCurveItem + +_logger = logging.getLogger(__name__) + + +class NegativeData(NamedTuple): + xx: numpy.ndarray + yy: numpy.ndarray + nb_points: int + + +class NegativeItem(ComputedCurveItem, plot_model.IncrementalComputableMixIn): + """This item use a curve item to negative it.""" + + def __init__(self, parent=None): + ComputedCurveItem.__init__(self, parent=parent) + plot_model.IncrementalComputableMixIn.__init__(self) + + def name(self) -> str: + return "Negative" + + def __getstate__(self): + state: Dict[str, Any] = {} + state.update(plot_model.ChildItem.__getstate__(self)) + state.update(plot_item_model.CurveMixIn.__getstate__(self)) + return state + + def __setstate__(self, state): + plot_model.ChildItem.__setstate__(self, state) + plot_item_model.CurveMixIn.__setstate__(self, state) + + def compute(self, scan: scan_model.Scan) -> Optional[NegativeData]: + sourceItem = self.source() + + xx = sourceItem.xArray(scan) + yy = sourceItem.yArray(scan) + if xx is None or yy is None: + return None + + size = min(len(xx), len(yy)) + return NegativeData(xx[0:size], -yy[0:size], size) + + def incrementalCompute( + self, previousResult: NegativeData, scan: scan_model.Scan + ) -> NegativeData: + """Compute a data using the previous value as basis""" + sourceItem = self.source() + xx = sourceItem.xArray(scan) + yy = sourceItem.yArray(scan) + if xx is None or yy is None: + raise ValueError("Non empty data expected") + + nb = previousResult.nb_points + if nb == len(xx) or nb == len(yy): + # obviously nothing to compute + return previousResult + + xx = xx[nb:] + yy = yy[nb:] + + nbInc = min(len(xx), len(yy)) + + xx = numpy.append(previousResult.xx, xx[: nbInc + 1]) + yy = numpy.append(previousResult.yy, -yy[: nbInc + 1]) + + result = NegativeData(xx, yy, nb + nbInc) + return result + + def displayName(self, axisName, scan: scan_model.Scan) -> str: + """Helper to reach the axis display name""" + sourceItem = self.source() + if axisName == "x": + return sourceItem.displayName("x", scan) + elif axisName == "y": + return "neg(%s)" % sourceItem.displayName("y", scan) + else: + assert False diff --git a/bliss/flint/filters/normalized.py b/bliss/flint/filters/normalized.py new file mode 100644 index 0000000000..0377f8b687 --- /dev/null +++ b/bliss/flint/filters/normalized.py @@ -0,0 +1,162 @@ +# -*- coding: utf-8 -*- +# +# This file is part of the bliss project +# +# Copyright (c) 2015-2022 Beamline Control Unit, ESRF +# Distributed under the GNU LGPLv3. See LICENSE for more info. + +""" +Implementation of a normalization of the data using a monitor channel +""" +from __future__ import annotations +from typing import Optional +from typing import Dict +from typing import Any + +import numpy +import logging + +from ..model import scan_model +from ..model import plot_model +from ..model import plot_item_model +from ..model.plot_item_model import getHashableSource + +_logger = logging.getLogger(__name__) + + +class NormalizedCurveItem(plot_model.ChildItem, plot_item_model.CurveMixIn): + """Curve based on a source item, normalized by a side channel.""" + + def __init__(self, parent=None): + plot_model.ChildItem.__init__(self, parent) + plot_item_model.CurveMixIn.__init__(self) + self.__monitor: Optional[plot_model.ChannelRef] = None + + def __getstate__(self): + state: Dict[str, Any] = {} + state.update(plot_model.ChildItem.__getstate__(self)) + state.update(plot_item_model.CurveMixIn.__getstate__(self)) + monitor = self.__monitor + if monitor is not None: + state["monitor"] = monitor.name() + return state + + def __setstate__(self, state): + plot_model.ChildItem.__setstate__(self, state) + plot_item_model.CurveMixIn.__setstate__(self, state) + monitorName = state.get("monitor") + if monitorName is not None: + channel = plot_model.ChannelRef(None, monitorName) + self.setMonitorChannel(channel) + + def name(self) -> str: + monitor = self.__monitor + if monitor is None: + return "Normalized" + else: + return "Normalized by %s" % monitor.name() + + def inputData(self): + return getHashableSource(self.source()) + getHashableSource(self.__monitor) + + def isValid(self): + return self.source() is not None and self.__monitor is not None + + def getScanValidation(self, scan: scan_model.Scan) -> Optional[str]: + """ + Returns None if everything is fine, else a message to explain the problem. + """ + xx = self.xArray(scan) + yy = self.yArray(scan) + monitor = self.__monitor + if monitor is not None: + if monitor.array(scan) is None: + return "No data for the monitor" + if xx is None and yy is None: + return "No data available for X and Y data" + elif xx is None: + return "No data available for X data" + elif yy is None: + return "No data available for Y data" + elif xx.ndim != 1: + return "Dimension of X data do not match" + elif yy.ndim != 1: + return "Dimension of Y data do not match" + elif len(xx) != len(yy): + return "Size of X and Y data do not match" + # It's fine + return None + + def monitorChannel(self) -> Optional[plot_model.ChannelRef]: + return self.__monitor + + def setMonitorChannel(self, channel: Optional[plot_model.ChannelRef]): + self.__monitor = channel + self._emitValueChanged(plot_model.ChangeEventType.Y_CHANNEL) + + def xData(self, scan: scan_model.Scan) -> Optional[scan_model.Data]: + source = self.source() + return source.xData(scan) + + def yData(self, scan: scan_model.Scan) -> Optional[scan_model.Data]: + source = self.source() + data = source.yArray(scan) + if data is None: + return None + monitorChannel = self.monitorChannel() + if monitorChannel is None: + return None + monitor = monitorChannel.array(scan) + if data is None or monitor is None: + return None + # FIXME: Could be cached + with numpy.errstate(all="ignore"): + yy = data / monitor + return scan_model.Data(None, yy) + + def setSource(self, source: plot_model.Item): + previousSource = self.source() + if previousSource is not None: + previousSource.valueChanged.disconnect(self.__sourceChanged) + plot_model.ChildItem.setSource(self, source) + if source is not None: + source.valueChanged.connect(self.__sourceChanged) + self.__sourceChanged(plot_model.ChangeEventType.X_CHANNEL) + self.__sourceChanged(plot_model.ChangeEventType.Y_CHANNEL) + + def __sourceChanged(self, eventType): + if eventType == plot_model.ChangeEventType.Y_CHANNEL: + self._emitValueChanged(plot_model.ChangeEventType.Y_CHANNEL) + if eventType == plot_model.ChangeEventType.X_CHANNEL: + self._emitValueChanged(plot_model.ChangeEventType.X_CHANNEL) + + def isAvailableInScan(self, scan: scan_model.Scan) -> bool: + """Returns true if this item is available in this scan. + + This only imply that the data source is available. + """ + if not plot_model.ChildItem.isAvailableInScan(self, scan): + return False + monitor = self.monitorChannel() + if monitor is not None: + if monitor.channel(scan) is None: + return False + return True + + def displayName(self, axisName, scan: scan_model.Scan) -> str: + """Helper to reach the axis display name""" + sourceItem = self.source() + monitor = self.__monitor + if axisName == "x": + return sourceItem.displayName("x", scan) + elif axisName == "y": + if monitor is None: + return "norm %s" % (sourceItem.displayName("y", scan)) + else: + monitorName = monitor.displayName(scan) + return "norm %s by %s" % ( + sourceItem.displayName("y", scan), + monitorName, + ) + else: + assert False diff --git a/bliss/flint/filters/normalized_zero_one.py b/bliss/flint/filters/normalized_zero_one.py new file mode 100644 index 0000000000..0b724a2409 --- /dev/null +++ b/bliss/flint/filters/normalized_zero_one.py @@ -0,0 +1,159 @@ +# -*- coding: utf-8 -*- +# +# This file is part of the bliss project +# +# Copyright (c) 2015-2022 Beamline Control Unit, ESRF +# Distributed under the GNU LGPLv3. See LICENSE for more info. + +""" +Implementation of a normalization of the data between 0..1 +""" +from __future__ import annotations +from typing import Optional +from typing import NamedTuple +from typing import Dict +from typing import Any + +import numpy +import logging + +from ..model import scan_model +from ..model import plot_model +from ..model import plot_item_model +from ..model.plot_item_model import ComputedCurveItem + +_logger = logging.getLogger(__name__) + + +class NormalizedZeroOneData(NamedTuple): + xx: numpy.ndarray + yy: numpy.ndarray + ymin: Optional[float] + ymax: Optional[float] + + def minmax(self, vmin, vmax): + """Returns reduced minmax between stored and other minmax. + + The update status is True if stored ymin and ymax differ from the + resulting minmax. + """ + ymin = self.ymin + ymax = self.ymax + updated = False + + if ymin != vmin: + if vmin is not None: + if ymin is None or vmin < ymin: + updated = True + ymin = vmin + if ymax != vmax: + if vmax is not None: + if ymax is None or vmax > ymax: + updated = True + ymax = vmax + + return ymin, ymax, updated + + +class NormalizedZeroOneItem(ComputedCurveItem, plot_model.IncrementalComputableMixIn): + """This item use the scan data to process result before displaying it. + + This normalize the range of the Y data in order to transform it between 0 + and 1. + """ + + def __init__(self, parent=None): + ComputedCurveItem.__init__(self, parent=parent) + plot_model.IncrementalComputableMixIn.__init__(self) + + def name(self) -> str: + return "Normalized range [0..1]" + + def __getstate__(self): + state: Dict[str, Any] = {} + state.update(plot_model.ChildItem.__getstate__(self)) + state.update(plot_item_model.CurveMixIn.__getstate__(self)) + return state + + def __setstate__(self, state): + plot_model.ChildItem.__setstate__(self, state) + plot_item_model.CurveMixIn.__setstate__(self, state) + + def _minmax(self, array): + if len(array) == 0: + vmin = None + vmax = None + else: + vmin = numpy.nanmin(array) + vmax = numpy.nanmax(array) + if not numpy.isfinite(vmin): + vmin = None + if not numpy.isfinite(vmax): + vmax = None + return vmin, vmax + + def compute(self, scan: scan_model.Scan) -> Optional[NormalizedZeroOneData]: + sourceItem = self.source() + + xx = sourceItem.xArray(scan) + yy = sourceItem.yArray(scan) + if xx is None or yy is None: + return None + + vmin, vmax = self._minmax(yy) + if vmin is None or vmax is None: + yy_normalized = yy + else: + yy_normalized = (yy - vmin) / (vmax - vmin) + return NormalizedZeroOneData(xx, yy_normalized, vmin, vmax) + + def incrementalCompute( + self, previousResult: NormalizedZeroOneData, scan: scan_model.Scan + ) -> NormalizedZeroOneData: + """Compute a data using the previous value as basis + + The derivative function expect 5 extra points before and after the + points it can compute. + + The last computed point have to be recomputed. + + This code is deeply coupled with the implementation of the derivative + function. + """ + sourceItem = self.source() + xx = sourceItem.xArray(scan) + yy = sourceItem.yArray(scan) + if xx is None or yy is None: + raise ValueError("Non empty data expected") + + nb = len(previousResult.yy) + if nb == len(yy): + # obviously nothing to compute + return previousResult + + new_yy = yy[len(previousResult.yy) :] + + vmin, vmax = self._minmax(new_yy) + vmin, vmax, updated = previousResult.minmax(vmin, vmax) + if not updated: + if vmin is None or vmax is None: + yy_new_normalized = yy + else: + yy_new_normalized = (yy - vmin) / (vmax - vmin) + yy_normalized = yy + yy_new_normalized + else: + if vmin is None or vmax is None: + yy_normalized = yy + else: + yy_normalized = (yy - vmin) / (vmax - vmin) + return NormalizedZeroOneData(xx, yy_normalized, vmin, vmax) + + def displayName(self, axisName, scan: scan_model.Scan) -> str: + """Helper to reach the axis display name""" + sourceItem = self.source() + if axisName == "x": + return sourceItem.displayName("x", scan) + elif axisName == "y": + return "norm(%s)" % sourceItem.displayName("y", scan) + else: + assert False diff --git a/bliss/flint/flint_api.py b/bliss/flint/flint_api.py index 94d0b36943..369ec185c9 100755 --- a/bliss/flint/flint_api.py +++ b/bliss/flint/flint_api.py @@ -31,7 +31,6 @@ from bliss.flint.helper import model_helper from bliss.flint.model import scan_model from bliss.flint.model import plot_model from bliss.flint.model import plot_item_model -from bliss.flint.model import plot_state_model from bliss.flint.model import flint_model from bliss.common import event from bliss.flint import config @@ -618,7 +617,7 @@ class FlintApi: # Search for previous user item if parentItem is not None: for userItem in list(model.items()): - if not isinstance(userItem, plot_state_model.UserValueItem): + if not isinstance(userItem, plot_item_model.UserValueItem): continue if not userItem.isChildOf(parentItem): continue @@ -644,7 +643,7 @@ class FlintApi: # It was not there before, so hide it parentItem.setVisible(False) - userItem = plot_state_model.UserValueItem(model) + userItem = plot_item_model.UserValueItem(model) userItem.setName(unique_name) userItem.setYArray(ydata) userItem.setSource(parentItem) diff --git a/bliss/flint/helper/style_helper.py b/bliss/flint/helper/style_helper.py index 987cc21b80..427f92cc18 100755 --- a/bliss/flint/helper/style_helper.py +++ b/bliss/flint/helper/style_helper.py @@ -20,7 +20,6 @@ from bliss.flint.model import scan_model from bliss.flint.model import flint_model from bliss.flint.model import plot_model from bliss.flint.model import plot_item_model -from bliss.flint.model import plot_state_model _logger = logging.getLogger(__name__) @@ -161,7 +160,7 @@ class DefaultStyleStrategy(plot_model.StyleStrategy): # Allocate a new color for everything color = self.pickColor(i) i += 1 - if isinstance(item, plot_state_model.CurveStatisticItem): + if isinstance(item, plot_item_model.CurveStatisticItem): style = plot_model.Style(lineStyle=":", lineColor=color) else: style = plot_model.Style(lineStyle="-.", lineColor=color) @@ -182,7 +181,7 @@ class DefaultStyleStrategy(plot_model.StyleStrategy): source = item.source() baseStyle = self.getStyleFromItem(source, scan) color = baseStyle.lineColor - if isinstance(item, plot_state_model.CurveStatisticItem): + if isinstance(item, plot_item_model.CurveStatisticItem): style = plot_model.Style(lineStyle=":", lineColor=color) else: style = plot_model.Style(lineStyle="-.", lineColor=color) @@ -206,7 +205,7 @@ class DefaultStyleStrategy(plot_model.StyleStrategy): source = item.source() baseStyle = self.getStyleFromItem(source, scan) color = baseStyle.lineColor - if isinstance(item, plot_state_model.CurveStatisticItem): + if isinstance(item, plot_item_model.CurveStatisticItem): style = plot_model.Style(lineStyle=":", lineColor=color) else: style = plot_model.Style(lineStyle="-.", lineColor=color) diff --git a/bliss/flint/model/plot_item_model.py b/bliss/flint/model/plot_item_model.py index c325bd6856..6ed7a6286e 100755 --- a/bliss/flint/model/plot_item_model.py +++ b/bliss/flint/model/plot_item_model.py @@ -22,11 +22,14 @@ from typing import Any from typing import List import numpy +import logging from . import scan_model from . import plot_model from . import style_model +_logger = logging.getLogger(__name__) + class ScalarPlot(plot_model.Plot): """Define that the relative scan contains data which have to be displayed @@ -687,3 +690,173 @@ class MotorPositionMarker(AxisPositionMarker): Created for compatibility since bliss 1.3 """ + + +def getHashableSource(obj: plot_model.Item): + while isinstance(obj, plot_model.ChildItem): + obj = obj.source() + if obj is None: + return tuple() + if isinstance(obj, plot_model.ChannelRef): + return (obj.name(),) + if isinstance(obj, CurveItem): + x = obj.xChannel() + y = obj.yChannel() + xName = None if x is None else x.name() + yName = None if y is None else y.name() + return (xName, yName) + else: + _logger.error("Source list not implemented for %s" % type(obj)) + return tuple() + + +class CurveStatisticItem(plot_model.ChildItem): + """Statistic displayed on a source item, depending on it y-axis.""" + + def inputData(self): + return getHashableSource(self.source()) + + def yAxis(self) -> str: + """Returns the name of the y-axis in which the statistic have to be displayed""" + source = self.source() + return source.yAxis() + + def setSource(self, source: plot_model.Item): + previousSource = self.source() + if previousSource is not None: + previousSource.valueChanged.disconnect(self.__sourceChanged) + plot_model.ChildItem.setSource(self, source) + if source is not None: + source.valueChanged.connect(self.__sourceChanged) + self.__sourceChanged(plot_model.ChangeEventType.YAXIS) + self.__sourceChanged(plot_model.ChangeEventType.X_CHANNEL) + self.__sourceChanged(plot_model.ChangeEventType.Y_CHANNEL) + + def __sourceChanged(self, eventType): + if eventType == plot_model.ChangeEventType.YAXIS: + self._emitValueChanged(plot_model.ChangeEventType.YAXIS) + if eventType == plot_model.ChangeEventType.Y_CHANNEL: + self._emitValueChanged(plot_model.ChangeEventType.Y_CHANNEL) + if eventType == plot_model.ChangeEventType.X_CHANNEL: + self._emitValueChanged(plot_model.ChangeEventType.X_CHANNEL) + + +class ComputedCurveItem(plot_model.ChildItem, CurveMixIn): + def __init__(self, parent=None): + plot_model.ChildItem.__init__(self, parent) + CurveMixIn.__init__(self) + + def inputData(self): + return getHashableSource(self.source()) + + def isResultValid(self, result): + return result is not None + + def xData(self, scan: scan_model.Scan) -> Optional[scan_model.Data]: + result = self.reachResult(scan) + if not self.isResultValid(result): + return None + data = result.xx + return scan_model.Data(None, data) + + def yData(self, scan: scan_model.Scan) -> Optional[scan_model.Data]: + result = self.reachResult(scan) + if not self.isResultValid(result): + return None + data = result.yy + return scan_model.Data(None, data) + + def setSource(self, source: plot_model.Item): + previousSource = self.source() + if previousSource is not None: + previousSource.valueChanged.disconnect(self.__sourceChanged) + plot_model.ChildItem.setSource(self, source) + if source is not None: + source.valueChanged.connect(self.__sourceChanged) + self.__sourceChanged(plot_model.ChangeEventType.X_CHANNEL) + self.__sourceChanged(plot_model.ChangeEventType.Y_CHANNEL) + + def __sourceChanged(self, eventType): + if eventType == plot_model.ChangeEventType.Y_CHANNEL: + self._emitValueChanged(plot_model.ChangeEventType.Y_CHANNEL) + if eventType == plot_model.ChangeEventType.X_CHANNEL: + self._emitValueChanged(plot_model.ChangeEventType.X_CHANNEL) + + +class UserValueItem(plot_model.ChildItem, CurveMixIn, plot_model.NotReused): + """This item is used to add to the plot data provided by the user. + + The y-data is custom and the x-data is provided by the linked item. + """ + + def __init__(self, parent=None): + plot_model.ChildItem.__init__(self, parent=parent) + CurveMixIn.__init__(self) + self.__name = "userdata" + self.__y = None + + def setName(self, name): + self.__name = name + + def name(self) -> str: + return self.__name + + def displayName(self, axisName, scan: scan_model.Scan) -> str: + if axisName == "x": + sourceItem = self.source() + return sourceItem.displayName("x", scan) + elif axisName == "y": + return self.__name + + def isValid(self): + return self.source() is not None and self.__y is not None + + def inputData(self): + return getHashableSource(self.source()) + + def xData(self, scan: scan_model.Scan) -> Optional[scan_model.Data]: + source = self.source() + if source is None: + return None + return source.xData(scan) + + def setYArray(self, array): + self.__y = array + self._emitValueChanged(plot_model.ChangeEventType.Y_CHANNEL) + + def yData(self, scan: scan_model.Scan) -> Optional[scan_model.Data]: + return scan_model.Data(None, self.__y) + + def getScanValidation(self, scan: scan_model.Scan) -> Optional[str]: + """ + Returns None if everything is fine, else a message to explain the problem. + """ + xx = self.xArray(scan) + yy = self.yArray(scan) + if xx is None and yy is None: + return "No data available for X and Y data" + elif xx is None: + return "No data available for X data" + elif yy is None: + return "No data available for Y data" + elif xx.ndim != 1: + return "Dimension of X data do not match" + elif yy.ndim != 1: + return "Dimension of Y data do not match" + elif len(xx) != len(yy): + return "Size of X and Y data do not match" + # It's fine + return None + + def setSource(self, source: plot_model.Item): + previousSource = self.source() + if previousSource is not None: + previousSource.valueChanged.disconnect(self.__sourceChanged) + plot_model.ChildItem.setSource(self, source) + if source is not None: + source.valueChanged.connect(self.__sourceChanged) + self.__sourceChanged(plot_model.ChangeEventType.X_CHANNEL) + + def __sourceChanged(self, eventType): + if eventType == plot_model.ChangeEventType.X_CHANNEL: + self._emitValueChanged(plot_model.ChangeEventType.X_CHANNEL) diff --git a/bliss/flint/model/plot_state_model.py b/bliss/flint/model/plot_state_model.py index 31713901d5..35a5e06166 100644 --- a/bliss/flint/model/plot_state_model.py +++ b/bliss/flint/model/plot_state_model.py @@ -5,859 +5,22 @@ # Copyright (c) 2015-2022 Beamline Control Unit, ESRF # Distributed under the GNU LGPLv3. See LICENSE for more info. -"""Contains implementation of concrete objects used to model plots. - -It exists 4 kinds of plots: curves, scatter, image, MCAs. Each plot contains -specific items. But it is not a constraint from the architecture. - -Here is a list of plot and item inheritance. - -.. image:: _static/flint/model/plot_model_item.png - :alt: Scan model - :align: center """ -from __future__ import annotations -from typing import Optional -from typing import NamedTuple -from typing import Dict -from typing import Any - -import numpy -import logging - -from . import scan_model -from . import plot_model -from . import plot_item_model -from ..utils import mathutils - -_logger = logging.getLogger(__name__) - - -def _getHashableSource(obj: plot_model.Item): - while isinstance(obj, plot_model.ChildItem): - obj = obj.source() - if obj is None: - return tuple() - if isinstance(obj, plot_model.ChannelRef): - return (obj.name(),) - if isinstance(obj, plot_item_model.CurveItem): - x = obj.xChannel() - y = obj.yChannel() - xName = None if x is None else x.name() - yName = None if y is None else y.name() - return (xName, yName) - else: - _logger.error("Source list not implemented for %s" % type(obj)) - return tuple() - - -class CurveStatisticItem(plot_model.ChildItem): - """Statistic displayed on a source item, depending on it y-axis.""" - - def inputData(self): - return _getHashableSource(self.source()) - - def yAxis(self) -> str: - """Returns the name of the y-axis in which the statistic have to be displayed""" - source = self.source() - return source.yAxis() - - def setSource(self, source: plot_model.Item): - previousSource = self.source() - if previousSource is not None: - previousSource.valueChanged.disconnect(self.__sourceChanged) - plot_model.ChildItem.setSource(self, source) - if source is not None: - source.valueChanged.connect(self.__sourceChanged) - self.__sourceChanged(plot_model.ChangeEventType.YAXIS) - self.__sourceChanged(plot_model.ChangeEventType.X_CHANNEL) - self.__sourceChanged(plot_model.ChangeEventType.Y_CHANNEL) - - def __sourceChanged(self, eventType): - if eventType == plot_model.ChangeEventType.YAXIS: - self._emitValueChanged(plot_model.ChangeEventType.YAXIS) - if eventType == plot_model.ChangeEventType.Y_CHANNEL: - self._emitValueChanged(plot_model.ChangeEventType.Y_CHANNEL) - if eventType == plot_model.ChangeEventType.X_CHANNEL: - self._emitValueChanged(plot_model.ChangeEventType.X_CHANNEL) - - -class DerivativeData(NamedTuple): - xx: numpy.ndarray - yy: numpy.ndarray - nb_points: int - - -class NegativeData(NamedTuple): - xx: numpy.ndarray - yy: numpy.ndarray - nb_points: int - - -class NormalizedZeroOneData(NamedTuple): - xx: numpy.ndarray - yy: numpy.ndarray - ymin: Optional[float] - ymax: Optional[float] - - def minmax(self, vmin, vmax): - """Returns reduced minmax between stored and other minmax. - - The update status is True if stored ymin and ymax differ from the - resulting minmax. - """ - ymin = self.ymin - ymax = self.ymax - updated = False - - if ymin != vmin: - if vmin is not None: - if ymin is None or vmin < ymin: - updated = True - ymin = vmin - if ymax != vmax: - if vmax is not None: - if ymax is None or vmax > ymax: - updated = True - ymax = vmax - - return ymin, ymax, updated - - -class ComputedCurveItem(plot_model.ChildItem, plot_item_model.CurveMixIn): - def __init__(self, parent=None): - plot_model.ChildItem.__init__(self, parent) - plot_item_model.CurveMixIn.__init__(self) - - def inputData(self): - return _getHashableSource(self.source()) - - def isResultValid(self, result): - return result is not None - - def xData(self, scan: scan_model.Scan) -> Optional[scan_model.Data]: - result = self.reachResult(scan) - if not self.isResultValid(result): - return None - data = result.xx - return scan_model.Data(None, data) - - def yData(self, scan: scan_model.Scan) -> Optional[scan_model.Data]: - result = self.reachResult(scan) - if not self.isResultValid(result): - return None - data = result.yy - return scan_model.Data(None, data) - - def setSource(self, source: plot_model.Item): - previousSource = self.source() - if previousSource is not None: - previousSource.valueChanged.disconnect(self.__sourceChanged) - plot_model.ChildItem.setSource(self, source) - if source is not None: - source.valueChanged.connect(self.__sourceChanged) - self.__sourceChanged(plot_model.ChangeEventType.X_CHANNEL) - self.__sourceChanged(plot_model.ChangeEventType.Y_CHANNEL) - - def __sourceChanged(self, eventType): - if eventType == plot_model.ChangeEventType.Y_CHANNEL: - self._emitValueChanged(plot_model.ChangeEventType.Y_CHANNEL) - if eventType == plot_model.ChangeEventType.X_CHANNEL: - self._emitValueChanged(plot_model.ChangeEventType.X_CHANNEL) - - -class DerivativeItem(ComputedCurveItem, plot_model.IncrementalComputableMixIn): - """This item use the scan data to process result before displaying it.""" - - EXTRA_POINTS = 5 - """Extra points needed before and after a single point to compute a result""" - - def __init__(self, parent=None): - ComputedCurveItem.__init__(self, parent=parent) - plot_model.IncrementalComputableMixIn.__init__(self) - - def name(self) -> str: - return "Derivative" - - def __getstate__(self): - state: Dict[str, Any] = {} - state.update(plot_model.ChildItem.__getstate__(self)) - state.update(plot_item_model.CurveMixIn.__getstate__(self)) - return state - - def __setstate__(self, state): - plot_model.ChildItem.__setstate__(self, state) - plot_item_model.CurveMixIn.__setstate__(self, state) - - def compute(self, scan: scan_model.Scan) -> Optional[DerivativeData]: - sourceItem = self.source() - - xx = sourceItem.xArray(scan) - yy = sourceItem.yArray(scan) - if xx is None or yy is None: - return None - - try: - derived = mathutils.derivate(xx, yy) - except Exception as e: - _logger.debug("Error while computing derivative", exc_info=True) - result = DerivativeData(numpy.array([]), numpy.array([]), len(xx)) - raise plot_model.ComputeError( - "Error while creating derivative.\n" + str(e), result=result - ) - - return DerivativeData(derived[0], derived[1], len(xx)) - - def incrementalCompute( - self, previousResult: DerivativeData, scan: scan_model.Scan - ) -> DerivativeData: - """Compute a data using the previous value as basis - - The derivative function expect 5 extra points before and after the - points it can compute. - - The last computed point have to be recomputed. - - This code is deeply coupled with the implementation of the derivative - function. - """ - sourceItem = self.source() - xx = sourceItem.xArray(scan) - yy = sourceItem.yArray(scan) - if xx is None or yy is None: - raise ValueError("Non empty data expected") - - nb = previousResult.nb_points - if nb == len(xx): - # obviously nothing to compute - return previousResult - nextNb = len(xx) - - # The last point have to be recomputed - LAST = 1 - - if len(xx) <= 2 * self.EXTRA_POINTS + LAST: - return DerivativeData(numpy.array([]), numpy.array([]), nextNb) - - if len(previousResult.xx) == 0: - # If there is no previous point, there is no need to compute it - LAST = 0 - - xx = xx[nb - 2 * self.EXTRA_POINTS - LAST :] - yy = yy[nb - 2 * self.EXTRA_POINTS - LAST :] - - derived = mathutils.derivate(xx, yy) - - xx = numpy.append(previousResult.xx[:-1], derived[0]) - yy = numpy.append(previousResult.yy[:-1], derived[1]) - - result = DerivativeData(xx, yy, nextNb) - return result - - def displayName(self, axisName, scan: scan_model.Scan) -> str: - """Helper to reach the axis display name""" - sourceItem = self.source() - if axisName == "x": - return sourceItem.displayName("x", scan) - elif axisName == "y": - return "d(%s)" % sourceItem.displayName("y", scan) - else: - assert False - - -class NegativeItem(ComputedCurveItem, plot_model.IncrementalComputableMixIn): - """This item use a curve item to negative it.""" - - def __init__(self, parent=None): - ComputedCurveItem.__init__(self, parent=parent) - plot_model.IncrementalComputableMixIn.__init__(self) - - def name(self) -> str: - return "Negative" - - def __getstate__(self): - state: Dict[str, Any] = {} - state.update(plot_model.ChildItem.__getstate__(self)) - state.update(plot_item_model.CurveMixIn.__getstate__(self)) - return state - - def __setstate__(self, state): - plot_model.ChildItem.__setstate__(self, state) - plot_item_model.CurveMixIn.__setstate__(self, state) - - def compute(self, scan: scan_model.Scan) -> Optional[NegativeData]: - sourceItem = self.source() - - xx = sourceItem.xArray(scan) - yy = sourceItem.yArray(scan) - if xx is None or yy is None: - return None - - size = min(len(xx), len(yy)) - return NegativeData(xx[0:size], -yy[0:size], size) - - def incrementalCompute( - self, previousResult: NegativeData, scan: scan_model.Scan - ) -> NegativeData: - """Compute a data using the previous value as basis""" - sourceItem = self.source() - xx = sourceItem.xArray(scan) - yy = sourceItem.yArray(scan) - if xx is None or yy is None: - raise ValueError("Non empty data expected") - - nb = previousResult.nb_points - if nb == len(xx) or nb == len(yy): - # obviously nothing to compute - return previousResult - - xx = xx[nb:] - yy = yy[nb:] - - nbInc = min(len(xx), len(yy)) - - xx = numpy.append(previousResult.xx, xx[: nbInc + 1]) - yy = numpy.append(previousResult.yy, -yy[: nbInc + 1]) - - result = NegativeData(xx, yy, nb + nbInc) - return result - - def displayName(self, axisName, scan: scan_model.Scan) -> str: - """Helper to reach the axis display name""" - sourceItem = self.source() - if axisName == "x": - return sourceItem.displayName("x", scan) - elif axisName == "y": - return "neg(%s)" % sourceItem.displayName("y", scan) - else: - assert False - - -class GaussianFitData(NamedTuple): - xx: numpy.ndarray - yy: numpy.ndarray - fit: mathutils.GaussianFitResult - - -class GaussianFitItem(ComputedCurveItem, plot_model.ComputableMixIn): - """This item use the scan data to process result before displaying it.""" - - def __getstate__(self): - state: Dict[str, Any] = {} - state.update(plot_model.ChildItem.__getstate__(self)) - state.update(plot_item_model.CurveMixIn.__getstate__(self)) - return state - - def __setstate__(self, state): - plot_model.ChildItem.__setstate__(self, state) - plot_item_model.CurveMixIn.__setstate__(self, state) - - def compute(self, scan: scan_model.Scan) -> Optional[GaussianFitData]: - sourceItem = self.source() - - xx = sourceItem.xArray(scan) - yy = sourceItem.yArray(scan) - if xx is None or yy is None: - return None - - try: - fit = mathutils.fit_gaussian(xx, yy) - except Exception as e: - _logger.debug("Error while computing gaussian fit", exc_info=True) - result = GaussianFitData(numpy.array([]), numpy.array([]), None) - raise plot_model.ComputeError( - "Error while creating gaussian fit.\n" + str(e), result=result - ) - - yy = fit.transform(xx) - return GaussianFitData(xx, yy, fit) - - def name(self) -> str: - return "Gaussian" - - def displayName(self, axisName, scan: scan_model.Scan) -> str: - """Helper to reach the axis display name""" - sourceItem = self.source() - if axisName == "x": - return sourceItem.displayName("x", scan) - elif axisName == "y": - return "gaussian(%s)" % sourceItem.displayName("y", scan) - else: - assert False - +Compatibility stored pickeled object. -class NormalizedZeroOneItem(ComputedCurveItem, plot_model.IncrementalComputableMixIn): - """This item use the scan data to process result before displaying it. +Everything was moved inside `bliss.flint.filters` for BLISS v1.11. - This normalize the range of the Y data in order to transform it between 0 - and 1. - """ - - def __init__(self, parent=None): - ComputedCurveItem.__init__(self, parent=parent) - plot_model.IncrementalComputableMixIn.__init__(self) - - def name(self) -> str: - return "Normalized range [0..1]" - - def __getstate__(self): - state: Dict[str, Any] = {} - state.update(plot_model.ChildItem.__getstate__(self)) - state.update(plot_item_model.CurveMixIn.__getstate__(self)) - return state - - def __setstate__(self, state): - plot_model.ChildItem.__setstate__(self, state) - plot_item_model.CurveMixIn.__setstate__(self, state) - - def _minmax(self, array): - if len(array) == 0: - vmin = None - vmax = None - else: - vmin = numpy.nanmin(array) - vmax = numpy.nanmax(array) - if not numpy.isfinite(vmin): - vmin = None - if not numpy.isfinite(vmax): - vmax = None - return vmin, vmax - - def compute(self, scan: scan_model.Scan) -> Optional[NormalizedZeroOneData]: - sourceItem = self.source() - - xx = sourceItem.xArray(scan) - yy = sourceItem.yArray(scan) - if xx is None or yy is None: - return None - - vmin, vmax = self._minmax(yy) - if vmin is None or vmax is None: - yy_normalized = yy - else: - yy_normalized = (yy - vmin) / (vmax - vmin) - return NormalizedZeroOneData(xx, yy_normalized, vmin, vmax) - - def incrementalCompute( - self, previousResult: NormalizedZeroOneData, scan: scan_model.Scan - ) -> NormalizedZeroOneData: - """Compute a data using the previous value as basis - - The derivative function expect 5 extra points before and after the - points it can compute. - - The last computed point have to be recomputed. - - This code is deeply coupled with the implementation of the derivative - function. - """ - sourceItem = self.source() - xx = sourceItem.xArray(scan) - yy = sourceItem.yArray(scan) - if xx is None or yy is None: - raise ValueError("Non empty data expected") - - nb = len(previousResult.yy) - if nb == len(yy): - # obviously nothing to compute - return previousResult - - new_yy = yy[len(previousResult.yy) :] - - vmin, vmax = self._minmax(new_yy) - vmin, vmax, updated = previousResult.minmax(vmin, vmax) - if not updated: - if vmin is None or vmax is None: - yy_new_normalized = yy - else: - yy_new_normalized = (yy - vmin) / (vmax - vmin) - yy_normalized = yy + yy_new_normalized - else: - if vmin is None or vmax is None: - yy_normalized = yy - else: - yy_normalized = (yy - vmin) / (vmax - vmin) - return NormalizedZeroOneData(xx, yy_normalized, vmin, vmax) - - def displayName(self, axisName, scan: scan_model.Scan) -> str: - """Helper to reach the axis display name""" - sourceItem = self.source() - if axisName == "x": - return sourceItem.displayName("x", scan) - elif axisName == "y": - return "norm(%s)" % sourceItem.displayName("y", scan) - else: - assert False - - -class MaxData(NamedTuple): - max_index: int - max_location_y: float - max_location_x: float - min_y_value: float - nb_points: int - - -class MaxCurveItem(CurveStatisticItem, plot_model.IncrementalComputableMixIn): - """Statistic identifying the maximum location of a curve.""" - - def name(self) -> str: - return "Max" - - def isResultValid(self, result): - return result is not None - - def compute(self, scan: scan_model.Scan) -> Optional[MaxData]: - sourceItem = self.source() - - xx = sourceItem.xArray(scan) - yy = sourceItem.yArray(scan) - if xx is None or yy is None: - return None - - max_index = numpy.argmax(yy) - min_y_value = numpy.min(yy) - max_location_x, max_location_y = xx[max_index], yy[max_index] - - result = MaxData( - max_index, max_location_y, max_location_x, min_y_value, len(xx) - ) - return result - - def incrementalCompute( - self, previousResult: MaxData, scan: scan_model.Scan - ) -> MaxData: - sourceItem = self.source() - - xx = sourceItem.xArray(scan) - yy = sourceItem.yArray(scan) - if xx is None or yy is None: - raise ValueError("Non empty data is expected") - - nb = previousResult.nb_points - if nb == len(xx): - # obviously nothing to compute - return previousResult - - xx = xx[nb:] - yy = yy[nb:] - - max_index = numpy.argmax(yy) - min_y_value = numpy.min(yy) - max_location_x, max_location_y = xx[max_index], yy[max_index] - max_index = max_index + nb - - if previousResult.min_y_value < min_y_value: - min_y_value = previousResult.min_y_value - - if previousResult.max_location_y > max_location_y: - # Update and return the previous result - return MaxData( - previousResult.max_index, - previousResult.max_location_y, - previousResult.max_location_x, - min_y_value, - nb + len(xx), - ) - - # Update and new return the previous result - result = MaxData( - max_index, max_location_y, max_location_x, min_y_value, nb + len(xx) - ) - return result - - -class MinData(NamedTuple): - min_index: int - min_location_y: float - min_location_x: float - max_y_value: float - nb_points: int - - -class MinCurveItem(CurveStatisticItem, plot_model.IncrementalComputableMixIn): - """Statistic identifying the minimum location of a curve.""" - - def name(self) -> str: - return "Min" - - def isResultValid(self, result): - return result is not None - - def compute(self, scan: scan_model.Scan) -> Optional[MinData]: - sourceItem = self.source() - - xx = sourceItem.xArray(scan) - yy = sourceItem.yArray(scan) - if xx is None or yy is None: - return None - - min_index = numpy.argmin(yy) - max_y_value = numpy.max(yy) - min_location_x, min_location_y = xx[min_index], yy[min_index] - - result = MinData( - min_index, min_location_y, min_location_x, max_y_value, len(xx) - ) - return result - - def incrementalCompute( - self, previousResult: MinData, scan: scan_model.Scan - ) -> MinData: - sourceItem = self.source() - - xx = sourceItem.xArray(scan) - yy = sourceItem.yArray(scan) - if xx is None or yy is None: - raise ValueError("Non empty data is expected") - - nb = previousResult.nb_points - if nb == len(xx): - # obviously nothing to compute - return previousResult - - xx = xx[nb:] - yy = yy[nb:] - - min_index = numpy.argmin(yy) - max_y_value = numpy.max(yy) - min_location_x, min_location_y = xx[min_index], yy[min_index] - min_index = min_index + nb - - if previousResult.max_y_value < max_y_value: - max_y_value = previousResult.max_y_value - - if previousResult.min_location_y < min_location_y: - # Update and return the previous result - return MinData( - previousResult.min_index, - previousResult.min_location_y, - previousResult.min_location_x, - max_y_value, - nb + len(xx), - ) - - # Update and new return the previous result - result = MinData( - min_index, min_location_y, min_location_x, max_y_value, nb + len(xx) - ) - return result - - -class NormalizedCurveItem(plot_model.ChildItem, plot_item_model.CurveMixIn): - """Curve based on a source item, normalized by a side channel.""" - - def __init__(self, parent=None): - plot_model.ChildItem.__init__(self, parent) - plot_item_model.CurveMixIn.__init__(self) - self.__monitor: Optional[plot_model.ChannelRef] = None - - def __getstate__(self): - state: Dict[str, Any] = {} - state.update(plot_model.ChildItem.__getstate__(self)) - state.update(plot_item_model.CurveMixIn.__getstate__(self)) - monitor = self.__monitor - if monitor is not None: - state["monitor"] = monitor.name() - return state - - def __setstate__(self, state): - plot_model.ChildItem.__setstate__(self, state) - plot_item_model.CurveMixIn.__setstate__(self, state) - monitorName = state.get("monitor") - if monitorName is not None: - channel = plot_model.ChannelRef(None, monitorName) - self.setMonitorChannel(channel) - - def name(self) -> str: - monitor = self.__monitor - if monitor is None: - return "Normalized" - else: - return "Normalized by %s" % monitor.name() - - def inputData(self): - return _getHashableSource(self.source()) + _getHashableSource(self.__monitor) - - def isValid(self): - return self.source() is not None and self.__monitor is not None - - def getScanValidation(self, scan: scan_model.Scan) -> Optional[str]: - """ - Returns None if everything is fine, else a message to explain the problem. - """ - xx = self.xArray(scan) - yy = self.yArray(scan) - monitor = self.__monitor - if monitor is not None: - if monitor.array(scan) is None: - return "No data for the monitor" - if xx is None and yy is None: - return "No data available for X and Y data" - elif xx is None: - return "No data available for X data" - elif yy is None: - return "No data available for Y data" - elif xx.ndim != 1: - return "Dimension of X data do not match" - elif yy.ndim != 1: - return "Dimension of Y data do not match" - elif len(xx) != len(yy): - return "Size of X and Y data do not match" - # It's fine - return None - - def monitorChannel(self) -> Optional[plot_model.ChannelRef]: - return self.__monitor - - def setMonitorChannel(self, channel: Optional[plot_model.ChannelRef]): - self.__monitor = channel - self._emitValueChanged(plot_model.ChangeEventType.Y_CHANNEL) - - def xData(self, scan: scan_model.Scan) -> Optional[scan_model.Data]: - source = self.source() - return source.xData(scan) - - def yData(self, scan: scan_model.Scan) -> Optional[scan_model.Data]: - source = self.source() - data = source.yArray(scan) - if data is None: - return None - monitorChannel = self.monitorChannel() - if monitorChannel is None: - return None - monitor = monitorChannel.array(scan) - if data is None or monitor is None: - return None - # FIXME: Could be cached - with numpy.errstate(all="ignore"): - yy = data / monitor - return scan_model.Data(None, yy) - - def setSource(self, source: plot_model.Item): - previousSource = self.source() - if previousSource is not None: - previousSource.valueChanged.disconnect(self.__sourceChanged) - plot_model.ChildItem.setSource(self, source) - if source is not None: - source.valueChanged.connect(self.__sourceChanged) - self.__sourceChanged(plot_model.ChangeEventType.X_CHANNEL) - self.__sourceChanged(plot_model.ChangeEventType.Y_CHANNEL) - - def __sourceChanged(self, eventType): - if eventType == plot_model.ChangeEventType.Y_CHANNEL: - self._emitValueChanged(plot_model.ChangeEventType.Y_CHANNEL) - if eventType == plot_model.ChangeEventType.X_CHANNEL: - self._emitValueChanged(plot_model.ChangeEventType.X_CHANNEL) - - def isAvailableInScan(self, scan: scan_model.Scan) -> bool: - """Returns true if this item is available in this scan. - - This only imply that the data source is available. - """ - if not plot_model.ChildItem.isAvailableInScan(self, scan): - return False - monitor = self.monitorChannel() - if monitor is not None: - if monitor.channel(scan) is None: - return False - return True - - def displayName(self, axisName, scan: scan_model.Scan) -> str: - """Helper to reach the axis display name""" - sourceItem = self.source() - monitor = self.__monitor - if axisName == "x": - return sourceItem.displayName("x", scan) - elif axisName == "y": - if monitor is None: - return "norm %s" % (sourceItem.displayName("y", scan)) - else: - monitorName = monitor.displayName(scan) - return "norm %s by %s" % ( - sourceItem.displayName("y", scan), - monitorName, - ) - else: - assert False - - -class UserValueItem( - plot_model.ChildItem, plot_item_model.CurveMixIn, plot_model.NotReused -): - """This item is used to add to the plot data provided by the user. - - The y-data is custom and the x-data is provided by the linked item. - """ - - def __init__(self, parent=None): - plot_model.ChildItem.__init__(self, parent=parent) - plot_item_model.CurveMixIn.__init__(self) - self.__name = "userdata" - self.__y = None - - def setName(self, name): - self.__name = name - - def name(self) -> str: - return self.__name - - def displayName(self, axisName, scan: scan_model.Scan) -> str: - if axisName == "x": - sourceItem = self.source() - return sourceItem.displayName("x", scan) - elif axisName == "y": - return self.__name - - def isValid(self): - return self.source() is not None and self.__y is not None - - def inputData(self): - return _getHashableSource(self.source()) - - def xData(self, scan: scan_model.Scan) -> Optional[scan_model.Data]: - source = self.source() - if source is None: - return None - return source.xData(scan) - - def setYArray(self, array): - self.__y = array - self._emitValueChanged(plot_model.ChangeEventType.Y_CHANNEL) - - def yData(self, scan: scan_model.Scan) -> Optional[scan_model.Data]: - return scan_model.Data(None, self.__y) - - def getScanValidation(self, scan: scan_model.Scan) -> Optional[str]: - """ - Returns None if everything is fine, else a message to explain the problem. - """ - xx = self.xArray(scan) - yy = self.yArray(scan) - if xx is None and yy is None: - return "No data available for X and Y data" - elif xx is None: - return "No data available for X data" - elif yy is None: - return "No data available for Y data" - elif xx.ndim != 1: - return "Dimension of X data do not match" - elif yy.ndim != 1: - return "Dimension of Y data do not match" - elif len(xx) != len(yy): - return "Size of X and Y data do not match" - # It's fine - return None - - def setSource(self, source: plot_model.Item): - previousSource = self.source() - if previousSource is not None: - previousSource.valueChanged.disconnect(self.__sourceChanged) - plot_model.ChildItem.setSource(self, source) - if source is not None: - source.valueChanged.connect(self.__sourceChanged) - self.__sourceChanged(plot_model.ChangeEventType.X_CHANNEL) +This module could be removed in few years. +""" - def __sourceChanged(self, eventType): - if eventType == plot_model.ChangeEventType.X_CHANNEL: - self._emitValueChanged(plot_model.ChangeEventType.X_CHANNEL) +from bliss.flint.model.plot_item_model import CurveStatisticItem # noqa +from bliss.flint.model.plot_item_model import ComputedCurveItem # noqa +from bliss.flint.model.plot_item_model import UserValueItem # noqa + +from bliss.flint.filters.derivative import DerivativeItem # noqa +from bliss.flint.filters.min import MinCurveItem # noqa +from bliss.flint.filters.max import MaxCurveItem # noqa +from bliss.flint.filters.negative import NegativeItem # noqa +from bliss.flint.filters.normalized_zero_one import NormalizedZeroOneItem # noqa +from bliss.flint.filters.gaussian_fit import GaussianFitItem # noqa +from bliss.flint.filters.normalized import NormalizedCurveItem # noqa diff --git a/bliss/flint/widgets/_property_tree_helper.py b/bliss/flint/widgets/_property_tree_helper.py index f02c6a629b..9715fe6963 100755 --- a/bliss/flint/widgets/_property_tree_helper.py +++ b/bliss/flint/widgets/_property_tree_helper.py @@ -17,7 +17,6 @@ from silx.gui import icons from bliss.flint.model import scan_model from bliss.flint.model import plot_model from bliss.flint.model import plot_item_model -from bliss.flint.model import plot_state_model class StandardRowItem(qt.QStandardItem): @@ -100,9 +99,9 @@ class ScanRowItem(StandardRowItem): icon = icons.getQIcon("flint:icons/device-image-roi") elif isinstance(plotItem, plot_item_model.ScatterItem): icon = icons.getQIcon("flint:icons/channel-curve") - elif isinstance(plotItem, plot_state_model.CurveStatisticItem): + elif isinstance(plotItem, plot_item_model.CurveStatisticItem): icon = icons.getQIcon("flint:icons/item-stats") - elif isinstance(plotItem, plot_state_model.UserValueItem): + elif isinstance(plotItem, plot_item_model.UserValueItem): icon = icons.getQIcon("flint:icons/item-channel") elif isinstance(plotItem, plot_item_model.CurveMixIn): icon = icons.getQIcon("flint:icons/item-func") diff --git a/bliss/flint/widgets/curve_plot.py b/bliss/flint/widgets/curve_plot.py index 2a2859b9ea..4f0a5a5eca 100755 --- a/bliss/flint/widgets/curve_plot.py +++ b/bliss/flint/widgets/curve_plot.py @@ -27,7 +27,8 @@ from bliss.flint.model import scan_model from bliss.flint.model import flint_model from bliss.flint.model import plot_model from bliss.flint.model import plot_item_model -from bliss.flint.model import plot_state_model +from bliss.flint.filters.min import MinCurveItem +from bliss.flint.filters.max import MaxCurveItem from bliss.flint.helper import scan_info_helper from bliss.flint.helper import model_helper from bliss.flint.utils import signalutils @@ -908,8 +909,8 @@ class CurvePlotWidget(plot_helper.PlotWidget): plot.addItem(curveItem) plotItems.append((legend, "curve")) - elif isinstance(item, plot_state_model.CurveStatisticItem): - if isinstance(item, plot_state_model.MaxCurveItem): + elif isinstance(item, plot_item_model.CurveStatisticItem): + if isinstance(item, MaxCurveItem): legend = str(item) + "/" + str(scan) result = item.reachResult(scan) if item.isResultValid(result): @@ -949,7 +950,7 @@ class CurvePlotWidget(plot_helper.PlotWidget): yaxis=item.yAxis(), ) plotItems.append((key, "marker")) - elif isinstance(item, plot_state_model.MinCurveItem): + elif isinstance(item, MinCurveItem): legend = str(item) + "/" + str(scan) result = item.reachResult(scan) if item.isResultValid(result): diff --git a/bliss/flint/widgets/curve_plot_property.py b/bliss/flint/widgets/curve_plot_property.py index f777b7cdf0..fbe9a400f8 100755 --- a/bliss/flint/widgets/curve_plot_property.py +++ b/bliss/flint/widgets/curve_plot_property.py @@ -23,13 +23,19 @@ from silx.gui import utils as qtutils from bliss.flint.model import flint_model from bliss.flint.model import plot_model from bliss.flint.model import plot_item_model -from bliss.flint.model import plot_state_model from bliss.flint.model import scan_model from bliss.flint.helper import model_helper, scan_history, scan_info_helper from bliss.flint.helper.style_helper import DefaultStyleStrategy from bliss.flint.utils import qmodelutils from bliss.flint.widgets.select_channel_dialog import SelectChannelDialog from bliss.flint.widgets.plot_model_edited import PlotModelEditAction +from bliss.flint.filters.derivative import DerivativeItem +from bliss.flint.filters.min import MinCurveItem +from bliss.flint.filters.max import MaxCurveItem +from bliss.flint.filters.negative import NegativeItem +from bliss.flint.filters.normalized_zero_one import NormalizedZeroOneItem +from bliss.flint.filters.gaussian_fit import GaussianFitItem +from bliss.flint.filters.normalized import NormalizedCurveItem from . import delegates from . import data_views from . import _property_tree_helper @@ -273,7 +279,7 @@ class _AddItemAction(qt.QWidgetAction): icon = icons.getQIcon("flint:icons/item-stats") action.setIcon(icon) action.triggered.connect( - functools.partial(self.__createChildItem, plot_state_model.MaxCurveItem) + functools.partial(self.__createChildItem, MaxCurveItem) ) menu.addAction(action) @@ -282,7 +288,7 @@ class _AddItemAction(qt.QWidgetAction): icon = icons.getQIcon("flint:icons/item-stats") action.setIcon(icon) action.triggered.connect( - functools.partial(self.__createChildItem, plot_state_model.MinCurveItem) + functools.partial(self.__createChildItem, MinCurveItem) ) menu.addAction(action) @@ -293,9 +299,7 @@ class _AddItemAction(qt.QWidgetAction): icon = icons.getQIcon("flint:icons/item-func") action.setIcon(icon) action.triggered.connect( - functools.partial( - self.__createChildItem, plot_state_model.DerivativeItem - ) + functools.partial(self.__createChildItem, DerivativeItem) ) menu.addAction(action) @@ -304,7 +308,7 @@ class _AddItemAction(qt.QWidgetAction): icon = icons.getQIcon("flint:icons/item-func") action.setIcon(icon) action.triggered.connect( - functools.partial(self.__createChildItem, plot_state_model.NegativeItem) + functools.partial(self.__createChildItem, NegativeItem) ) menu.addAction(action) @@ -313,9 +317,7 @@ class _AddItemAction(qt.QWidgetAction): icon = icons.getQIcon("flint:icons/item-func") action.setIcon(icon) action.triggered.connect( - functools.partial( - self.__createChildItem, plot_state_model.GaussianFitItem - ) + functools.partial(self.__createChildItem, GaussianFitItem) ) menu.addAction(action) @@ -331,9 +333,7 @@ class _AddItemAction(qt.QWidgetAction): icon = icons.getQIcon("flint:icons/item-func") action.setIcon(icon) action.triggered.connect( - functools.partial( - self.__createChildItem, plot_state_model.NormalizedZeroOneItem - ) + functools.partial(self.__createChildItem, NormalizedZeroOneItem) ) menu.addAction(action) else: @@ -379,7 +379,7 @@ class _AddItemAction(qt.QWidgetAction): plotModel = parentItem.plot() if plotModel is None: return - newItem = plot_state_model.NormalizedCurveItem(plotModel) + newItem = NormalizedCurveItem(plotModel) channel = plot_model.ChannelRef(plotModel, monitorName) newItem.setMonitorChannel(channel) newItem.setSource(parentItem) @@ -633,7 +633,7 @@ class _DataItem(_property_tree_helper.ScanRowItem): # self.__updateXAxisStyle(False, None) useXAxis = False self.__updateXAxisStyle(False) - elif isinstance(plotItem, plot_state_model.CurveStatisticItem): + elif isinstance(plotItem, plot_item_model.CurveStatisticItem): useXAxis = False self.__updateXAxisStyle(False) diff --git a/bliss/flint/widgets/one_dim_plot_property.py b/bliss/flint/widgets/one_dim_plot_property.py index 54d8c523ac..dfaa2b9cb9 100755 --- a/bliss/flint/widgets/one_dim_plot_property.py +++ b/bliss/flint/widgets/one_dim_plot_property.py @@ -21,7 +21,6 @@ from silx.gui import utils as qtutils from bliss.flint.model import flint_model from bliss.flint.model import plot_model from bliss.flint.model import plot_item_model -from bliss.flint.model import plot_state_model from bliss.flint.model import scan_model from bliss.flint.helper import model_helper, scan_history, scan_info_helper from bliss.flint.helper.style_helper import DefaultStyleStrategy @@ -280,7 +279,7 @@ class _DataItem(_property_tree_helper.ScanRowItem): # self.__updateXAxisStyle(False, None) useXAxis = False self.__updateXAxisStyle(False) - elif isinstance(plotItem, plot_state_model.CurveStatisticItem): + elif isinstance(plotItem, plot_item_model.CurveStatisticItem): useXAxis = False self.__updateXAxisStyle(False) diff --git a/bliss/flint/widgets/utils/plot_helper.py b/bliss/flint/widgets/utils/plot_helper.py index 8dc04851be..65457dfdd8 100644 --- a/bliss/flint/widgets/utils/plot_helper.py +++ b/bliss/flint/widgets/utils/plot_helper.py @@ -29,7 +29,7 @@ from silx.gui.plot.items.image_aggregated import ImageDataAggregated from bliss.flint.model import plot_model from bliss.flint.model import plot_item_model -from bliss.flint.model import plot_state_model +from bliss.flint.filters.gaussian_fit import GaussianFitItem from bliss.flint.model import scan_model from bliss.flint.utils import signalutils from bliss.flint.widgets.extended_dock_widget import ExtendedDockWidget @@ -573,7 +573,7 @@ class FlintCurve(Curve, FlintItemMixIn):
  • {xName}: {xValue}
  • """ - if isinstance(plotItem, plot_state_model.GaussianFitItem): + if isinstance(plotItem, GaussianFitItem): result = plotItem.reachResult(scan) if result is not None: text += f""" diff --git a/tests/qt/flint/helper/test_model_helper.py b/tests/qt/flint/helper/test_model_helper.py index af24383627..18ed22e633 100755 --- a/tests/qt/flint/helper/test_model_helper.py +++ b/tests/qt/flint/helper/test_model_helper.py @@ -2,9 +2,11 @@ import typing from bliss.flint.helper import model_helper -from bliss.flint.model import scan_model, plot_state_model +from bliss.flint.model import scan_model from bliss.flint.model import plot_model from bliss.flint.model import plot_item_model +from bliss.flint.filters.derivative import DerivativeItem +from bliss.flint.filters.gaussian_fit import GaussianFitItem def test_clone_channel_ref(): @@ -539,7 +541,7 @@ def test_filter_used_data_items(): item.setYChannel(channel) plot.addItem(item) - item = plot_state_model.GaussianFitItem(plot) + item = GaussianFitItem(plot) plot.addItem(item) item = plot_item_model.AxisPositionMarker(plot) @@ -727,11 +729,11 @@ def test_copy_config_tree__updated_root_item(): item = add_item(source, "x", "y1") item.setYAxis("right") - item2 = plot_state_model.DerivativeItem(source) + item2 = DerivativeItem(source) item2.setSource(item) source.addItem(item2) - item3 = plot_state_model.GaussianFitItem(source) + item3 = GaussianFitItem(source) item3.setSource(item2) source.addItem(item3) @@ -744,7 +746,7 @@ def test_copy_config_tree__updated_root_item(): assert destItem.yAxis() == "right" items = list(destination.items()) assert len(items) == 5 - gaussianItem = [i for i in items if type(i) == plot_state_model.GaussianFitItem] + gaussianItem = [i for i in items if type(i) == GaussianFitItem] assert len(gaussianItem) == 1 assert gaussianItem[0].source().source().yChannel().name() == "y1" @@ -770,11 +772,11 @@ def test_copy_config_tree__created_root_item(): item = add_item(source, "x", "y1") item.setYAxis("right") - item2 = plot_state_model.DerivativeItem(source) + item2 = DerivativeItem(source) item2.setSource(item) source.addItem(item2) - item3 = plot_state_model.GaussianFitItem(source) + item3 = GaussianFitItem(source) item3.setSource(item2) source.addItem(item3) @@ -786,7 +788,7 @@ def test_copy_config_tree__created_root_item(): assert destItem.yAxis() == "right" items = list(destination.items()) assert len(items) == 4 - gaussianItem = [i for i in items if type(i) == plot_state_model.GaussianFitItem] + gaussianItem = [i for i in items if type(i) == GaussianFitItem] assert len(gaussianItem) == 1 assert gaussianItem[0].source().source().yChannel().name() == "y1" diff --git a/tests/qt/flint/model/test_persistence.py b/tests/qt/flint/model/test_persistence.py index de4370f2bf..e8945d27d6 100644 --- a/tests/qt/flint/model/test_persistence.py +++ b/tests/qt/flint/model/test_persistence.py @@ -7,10 +7,14 @@ import pickle import pytest from bliss.flint.model import plot_item_model -from bliss.flint.model import plot_state_model from bliss.flint.model import plot_model from bliss.flint.model import style_model from bliss.flint.helper import style_helper +from bliss.flint.filters.derivative import DerivativeItem +from bliss.flint.filters.min import MinCurveItem +from bliss.flint.filters.max import MaxCurveItem +from bliss.flint.filters.gaussian_fit import GaussianFitItem +from bliss.flint.filters.normalized import NormalizedCurveItem @pytest.mark.parametrize( @@ -28,12 +32,12 @@ from bliss.flint.helper import style_helper plot_item_model.ScatterItem, plot_item_model.McaItem, plot_item_model.ImageItem, - plot_state_model.DerivativeItem, - plot_state_model.GaussianFitItem, - plot_state_model.CurveStatisticItem, - plot_state_model.MinCurveItem, - plot_state_model.NormalizedCurveItem, - plot_state_model.MaxCurveItem, + DerivativeItem, + GaussianFitItem, + plot_item_model.CurveStatisticItem, + MinCurveItem, + NormalizedCurveItem, + MaxCurveItem, plot_item_model.MotorPositionMarker, plot_item_model.AxisPositionMarker, style_helper.DefaultStyleStrategy, @@ -47,13 +51,13 @@ def test_dumps_loads(tested): def test_normalized_curve_item(): - item = plot_state_model.NormalizedCurveItem() + item = NormalizedCurveItem() channelMonitor = plot_model.ChannelRef(None, "foo") item.setMonitorChannel(channelMonitor) data = pickle.dumps(item) result = pickle.loads(data) - assert isinstance(result, plot_state_model.NormalizedCurveItem) + assert isinstance(result, NormalizedCurveItem) assert result.monitorChannel() is not None assert result.monitorChannel().name() == "foo" diff --git a/tests/qt/flint/model/test_plot_item_model.py b/tests/qt/flint/model/test_plot_item_model.py index 7b9dc104d1..46323a1afd 100755 --- a/tests/qt/flint/model/test_plot_item_model.py +++ b/tests/qt/flint/model/test_plot_item_model.py @@ -2,9 +2,10 @@ import numpy from bliss.flint.model import plot_item_model -from bliss.flint.model import plot_state_model from bliss.flint.model import plot_model from bliss.flint.model import scan_model +from bliss.flint.filters.derivative import DerivativeItem +from bliss.flint.filters.max import MaxCurveItem def test_picklable(): @@ -16,12 +17,12 @@ def test_picklable(): item.setYChannel(plot_model.ChannelRef(None, "y")) plot.addItem(item) - item2 = plot_state_model.DerivativeItem(plot) + item2 = DerivativeItem(plot) item2.setYAxis("right") item2.setSource(item) plot.addItem(item2) - item3 = plot_state_model.MaxCurveItem(plot) + item3 = MaxCurveItem(plot) item3.setSource(item2) plot.addItem(item3) import pickle diff --git a/tests/qt/flint/model/test_plot_state_model.py b/tests/qt/flint/model/test_plot_state_model.py index 405bffbd92..8694acdf41 100644 --- a/tests/qt/flint/model/test_plot_state_model.py +++ b/tests/qt/flint/model/test_plot_state_model.py @@ -2,10 +2,14 @@ import numpy from silx.gui import qt -from bliss.flint.model import plot_state_model from bliss.flint.model import plot_item_model from bliss.flint.model import plot_model from bliss.flint.model import scan_model +from bliss.flint.filters.derivative import DerivativeItem +from bliss.flint.filters.min import MinCurveItem +from bliss.flint.filters.max import MaxCurveItem +from bliss.flint.filters.normalized_zero_one import NormalizedZeroOneItem +from bliss.flint.filters.normalized import NormalizedCurveItem class CurveMock(qt.QObject, plot_item_model.CurveMixIn): @@ -38,7 +42,7 @@ def test_max_compute(): yy = [0, -10, 2, 5, 9, 500, 100] xx = numpy.arange(len(yy)) * 10 - item = plot_state_model.MaxCurveItem() + item = MaxCurveItem() curveItem = CurveMock(xx=xx, yy=yy) item.setSource(curveItem) @@ -55,7 +59,7 @@ def test_min_compute(): yy = [0, -10, 2, 5, 9, 500, 100] xx = numpy.arange(len(yy)) * 10 - item = plot_state_model.MinCurveItem() + item = MinCurveItem() curveItem = CurveMock(xx=xx, yy=yy) item.setSource(curveItem) @@ -73,7 +77,7 @@ def test_max_incremental_compute_1(): yy = [0, -10, 2, 5, 9, 500, 100] xx = numpy.arange(len(yy)) * 10 - item = plot_state_model.MaxCurveItem() + item = MaxCurveItem() curveItem = CurveMock(xx=xx[: len(xx) // 2], yy=yy[: len(xx) // 2]) item.setSource(curveItem) result = item.compute(scan) @@ -95,7 +99,7 @@ def test_max_incremental_compute_2(): yy = [0, 10, 500, 5, 9, -10, 100] xx = numpy.arange(len(yy)) * 10 - item = plot_state_model.MaxCurveItem() + item = MaxCurveItem() curveItem = CurveMock(xx=xx[: len(xx) // 2], yy=yy[: len(xx) // 2]) item.setSource(curveItem) result = item.compute(scan) @@ -118,15 +122,12 @@ def test_derivative_compute(): yy = numpy.cumsum(yy) xx = numpy.arange(len(yy)) * 10 - item = plot_state_model.DerivativeItem() + item = DerivativeItem() curveItem = CurveMock(xx=xx, yy=yy) item.setSource(curveItem) result = item.compute(scan) assert result is not None - assert ( - len(item.xArray(scan)) - == len(xx) - plot_state_model.DerivativeItem.EXTRA_POINTS * 2 - ) + assert len(item.xArray(scan)) == len(xx) - DerivativeItem.EXTRA_POINTS * 2 def test_derivative_incremental_compute(): @@ -136,7 +137,7 @@ def test_derivative_incremental_compute(): xx = numpy.arange(len(yy)) * 10 scan = scan_model.Scan() - item = plot_state_model.DerivativeItem() + item = DerivativeItem() curveItem = CurveMock(xx=xx, yy=yy) item.setSource(curveItem) expected = item.compute(scan) @@ -144,7 +145,7 @@ def test_derivative_incremental_compute(): result = None for i in [0, 5, 10, 15, 20, 25, len(xx)]: scan = scan_model.Scan() - item = plot_state_model.DerivativeItem() + item = DerivativeItem() curveItem = CurveMock(xx=xx[0:i], yy=yy[0:i]) item.setSource(curveItem) if result is None: @@ -168,7 +169,7 @@ def test_no_normalized_curve_item(): curveItem = CurveMock(xx=xx, yy=yy) scan = scan_model.Scan() - item = plot_state_model.NormalizedCurveItem() + item = NormalizedCurveItem() item.setSource(curveItem) assert item.yData(scan) is None @@ -183,7 +184,7 @@ def test_normalized_curve_item(): channelMonitor = ChannelMock(monitor) scan = scan_model.Scan() - item = plot_state_model.NormalizedCurveItem() + item = NormalizedCurveItem() item.setSource(curveItem) item.setMonitorChannel(channelMonitor) resulty = item.yData(scan).array() @@ -199,7 +200,7 @@ def test_normalizezeroone_compute(): xx = numpy.array([0, 1, 2, 3, 4]) expected = (yy + 100) / 200 - item = plot_state_model.NormalizedZeroOneItem() + item = NormalizedZeroOneItem() curveItem = CurveMock(xx=xx, yy=yy) item.setSource(curveItem) result = item.compute(scan) @@ -214,7 +215,7 @@ def test_normalizezeroone_incremental_compute(): xx = numpy.array([0, 1, 2, 3, 4]) scan = scan_model.Scan() - item = plot_state_model.NormalizedZeroOneItem() + item = NormalizedZeroOneItem() curveItem = CurveMock(xx=xx, yy=yy) item.setSource(curveItem) expected = item.compute(scan) @@ -222,7 +223,7 @@ def test_normalizezeroone_incremental_compute(): result = None for i in [0, 1, 4, len(xx)]: scan = scan_model.Scan() - item = plot_state_model.NormalizedZeroOneItem() + item = NormalizedZeroOneItem() curveItem = CurveMock(xx=xx[0:i], yy=yy[0:i]) item.setSource(curveItem) if result is None: -- GitLab From 3960e2fdf611cdca4768cb56c96920dc88ec9aae Mon Sep 17 00:00:00 2001 From: Valentin Valls Date: Wed, 18 May 2022 17:29:20 +0200 Subject: [PATCH 2/3] Pluginify the filters in the GUI --- bliss/flint/filters/derivative.py | 3 + bliss/flint/filters/gaussian_fit.py | 3 + bliss/flint/filters/max.py | 3 + bliss/flint/filters/min.py | 3 + bliss/flint/filters/negative.py | 3 + bliss/flint/filters/normalized.py | 3 + bliss/flint/filters/normalized_zero_one.py | 3 + bliss/flint/widgets/curve_plot_property.py | 86 ++++++---------------- 8 files changed, 45 insertions(+), 62 deletions(-) diff --git a/bliss/flint/filters/derivative.py b/bliss/flint/filters/derivative.py index 985cb5a459..028cd0c97b 100644 --- a/bliss/flint/filters/derivative.py +++ b/bliss/flint/filters/derivative.py @@ -35,6 +35,9 @@ class DerivativeData(NamedTuple): class DerivativeItem(ComputedCurveItem, plot_model.IncrementalComputableMixIn): """This item use the scan data to process result before displaying it.""" + NAME = "Derivative function" + ICON_NAME = "flint:icons/item-func" + EXTRA_POINTS = 5 """Extra points needed before and after a single point to compute a result""" diff --git a/bliss/flint/filters/gaussian_fit.py b/bliss/flint/filters/gaussian_fit.py index 6c710cc96f..6bea9575c9 100644 --- a/bliss/flint/filters/gaussian_fit.py +++ b/bliss/flint/filters/gaussian_fit.py @@ -35,6 +35,9 @@ class GaussianFitData(NamedTuple): class GaussianFitItem(ComputedCurveItem, plot_model.ComputableMixIn): """This item use the scan data to process result before displaying it.""" + NAME = "Gaussian fit" + ICON_NAME = "flint:icons/item-func" + def __getstate__(self): state: Dict[str, Any] = {} state.update(plot_model.ChildItem.__getstate__(self)) diff --git a/bliss/flint/filters/max.py b/bliss/flint/filters/max.py index d576462ea1..f39874f397 100644 --- a/bliss/flint/filters/max.py +++ b/bliss/flint/filters/max.py @@ -33,6 +33,9 @@ class MaxData(NamedTuple): class MaxCurveItem(CurveStatisticItem, plot_model.IncrementalComputableMixIn): """Statistic identifying the maximum location of a curve.""" + NAME = "Max marker" + ICON_NAME = "flint:icons/item-stats" + def name(self) -> str: return "Max" diff --git a/bliss/flint/filters/min.py b/bliss/flint/filters/min.py index 15643239b1..3146d534b7 100644 --- a/bliss/flint/filters/min.py +++ b/bliss/flint/filters/min.py @@ -33,6 +33,9 @@ class MinData(NamedTuple): class MinCurveItem(CurveStatisticItem, plot_model.IncrementalComputableMixIn): """Statistic identifying the minimum location of a curve.""" + NAME = "Min marker" + ICON_NAME = "flint:icons/item-stats" + def name(self) -> str: return "Min" diff --git a/bliss/flint/filters/negative.py b/bliss/flint/filters/negative.py index 6547cb5fb7..26992ea795 100644 --- a/bliss/flint/filters/negative.py +++ b/bliss/flint/filters/negative.py @@ -34,6 +34,9 @@ class NegativeData(NamedTuple): class NegativeItem(ComputedCurveItem, plot_model.IncrementalComputableMixIn): """This item use a curve item to negative it.""" + NAME = "Negative function" + ICON_NAME = "flint:icons/item-func" + def __init__(self, parent=None): ComputedCurveItem.__init__(self, parent=parent) plot_model.IncrementalComputableMixIn.__init__(self) diff --git a/bliss/flint/filters/normalized.py b/bliss/flint/filters/normalized.py index 0377f8b687..46b4e79931 100644 --- a/bliss/flint/filters/normalized.py +++ b/bliss/flint/filters/normalized.py @@ -27,6 +27,9 @@ _logger = logging.getLogger(__name__) class NormalizedCurveItem(plot_model.ChildItem, plot_item_model.CurveMixIn): """Curve based on a source item, normalized by a side channel.""" + NAME = "Normalized function" + ICON_NAME = "flint:icons/item-func" + def __init__(self, parent=None): plot_model.ChildItem.__init__(self, parent) plot_item_model.CurveMixIn.__init__(self) diff --git a/bliss/flint/filters/normalized_zero_one.py b/bliss/flint/filters/normalized_zero_one.py index 0b724a2409..611b836438 100644 --- a/bliss/flint/filters/normalized_zero_one.py +++ b/bliss/flint/filters/normalized_zero_one.py @@ -62,6 +62,9 @@ class NormalizedZeroOneItem(ComputedCurveItem, plot_model.IncrementalComputableM and 1. """ + NAME = "Normalized range" + ICON_NAME = "flint:icons/item-func" + def __init__(self, parent=None): ComputedCurveItem.__init__(self, parent=parent) plot_model.IncrementalComputableMixIn.__init__(self) diff --git a/bliss/flint/widgets/curve_plot_property.py b/bliss/flint/widgets/curve_plot_property.py index fbe9a400f8..9a1ef250fe 100755 --- a/bliss/flint/widgets/curve_plot_property.py +++ b/bliss/flint/widgets/curve_plot_property.py @@ -270,72 +270,34 @@ class _AddItemAction(qt.QWidgetAction): menu: qt.QMenu = self.sender() menu.clear() + availableItemFactory = { + MaxCurveItem: None, + MinCurveItem: None, + DerivativeItem: None, + NegativeItem: None, + GaussianFitItem: None, + NormalizedCurveItem: self.__createNormalized, + NormalizedZeroOneItem: None, + } + item = self.parent().selectedPlotItem() if isinstance(item, plot_item_model.CurveMixIn): menu.addSection("Statistics") - action = qt.QAction(self) - action.setText("Max marker") - icon = icons.getQIcon("flint:icons/item-stats") - action.setIcon(icon) - action.triggered.connect( - functools.partial(self.__createChildItem, MaxCurveItem) - ) - menu.addAction(action) - - action = qt.QAction(self) - action.setText("Min marker") - icon = icons.getQIcon("flint:icons/item-stats") - action.setIcon(icon) - action.triggered.connect( - functools.partial(self.__createChildItem, MinCurveItem) - ) - menu.addAction(action) - - menu.addSection("Functions") - - action = qt.QAction(self) - action.setText("Derivative function") - icon = icons.getQIcon("flint:icons/item-func") - action.setIcon(icon) - action.triggered.connect( - functools.partial(self.__createChildItem, DerivativeItem) - ) - menu.addAction(action) - - action = qt.QAction(self) - action.setText("Negative function") - icon = icons.getQIcon("flint:icons/item-func") - action.setIcon(icon) - action.triggered.connect( - functools.partial(self.__createChildItem, NegativeItem) - ) - menu.addAction(action) - - action = qt.QAction(self) - action.setText("Gaussian fit") - icon = icons.getQIcon("flint:icons/item-func") - action.setIcon(icon) - action.triggered.connect( - functools.partial(self.__createChildItem, GaussianFitItem) - ) - menu.addAction(action) - - action = qt.QAction(self) - action.setText("Normalized function") - icon = icons.getQIcon("flint:icons/item-func") - action.setIcon(icon) - action.triggered.connect(self.__createNormalized) - menu.addAction(action) - - action = qt.QAction(self) - action.setText("Normalized range") - icon = icons.getQIcon("flint:icons/item-func") - action.setIcon(icon) - action.triggered.connect( - functools.partial(self.__createChildItem, NormalizedZeroOneItem) - ) - menu.addAction(action) + for className, chassFactory in availableItemFactory.items(): + try: + name = className.NAME + except Exception: + name = className.__name__ + + if chassFactory is None: + chassFactory = functools.partial(self.__createChildItem, className) + action = qt.QAction(self) + action.setText(name) + icon = icons.getQIcon("flint:icons/item-stats") + action.setIcon(icon) + action.triggered.connect(chassFactory) + menu.addAction(action) else: action = qt.QAction(self) action.setText("No available items") -- GitLab From 8eef7392b2ba764ae484fcbde9ac5a5eb24fccf1 Mon Sep 17 00:00:00 2001 From: Valentin Valls Date: Wed, 18 May 2022 18:08:17 +0200 Subject: [PATCH 3/3] Rework item creation UI as an item param editor --- bliss/flint/widgets/curve_plot_property.py | 71 ++++++++++++---------- 1 file changed, 38 insertions(+), 33 deletions(-) diff --git a/bliss/flint/widgets/curve_plot_property.py b/bliss/flint/widgets/curve_plot_property.py index 9a1ef250fe..3dbe021e73 100755 --- a/bliss/flint/widgets/curve_plot_property.py +++ b/bliss/flint/widgets/curve_plot_property.py @@ -270,13 +270,13 @@ class _AddItemAction(qt.QWidgetAction): menu: qt.QMenu = self.sender() menu.clear() - availableItemFactory = { + availableItemParamEditor = { MaxCurveItem: None, MinCurveItem: None, DerivativeItem: None, NegativeItem: None, GaussianFitItem: None, - NormalizedCurveItem: self.__createNormalized, + NormalizedCurveItem: self.__editNormalizedParams, NormalizedZeroOneItem: None, } @@ -284,14 +284,14 @@ class _AddItemAction(qt.QWidgetAction): if isinstance(item, plot_item_model.CurveMixIn): menu.addSection("Statistics") - for className, chassFactory in availableItemFactory.items(): + for className, editor in availableItemParamEditor.items(): try: name = className.NAME except Exception: name = className.__name__ - - if chassFactory is None: - chassFactory = functools.partial(self.__createChildItem, className) + chassFactory = functools.partial( + self.__createChildItem, className, editor + ) action = qt.QAction(self) action.setText(name) icon = icons.getQIcon("flint:icons/item-stats") @@ -307,23 +307,39 @@ class _AddItemAction(qt.QWidgetAction): def __selectionChanged(self, current: plot_model.Item): self.defaultWidget().setEnabled(current is not None) - def __createChildItem(self, itemClass): + def __createChildItem(self, itemClass, itemEditor): parentItem = self.parent().selectedPlotItem() - if parentItem is not None: - plotModel = parentItem.plot() - newItem = itemClass(plotModel) - newItem.setSource(parentItem) - with plotModel.transaction(): - plotModel.addItem(newItem) - # FIXME: It would be better to make it part of the model - plotModel.tagUserEditTime() + if parentItem is None: + return + plotModel = parentItem.plot() + newItem = itemClass(plotModel) + if itemEditor is not None: + result = itemEditor(newItem) + if not result: + newItem.deleteLater() + return - def __createNormalized(self): + # Sanity check as we did a user selection in between it is not atomic parentItem = self.parent().selectedPlotItem() if parentItem is None: + newItem.deleteLater() return - # Avoid strong reference - parentItem = None + plotModel = parentItem.plot() + if plotModel is None: + newItem.deleteLater() + return + + newItem.setSource(parentItem) + with plotModel.transaction(): + plotModel.addItem(newItem) + # FIXME: It would be better to make it part of the model + plotModel.tagUserEditTime() + + def __editNormalizedParams(self, item: NormalizedCurveItem): + """Edit NormalizedCurveItem parameters. + + Returns true if the edition succeeded, else false. + """ parentWidget = self.parent() scan = parentWidget.scan() dialog = SelectChannelDialog(parentWidget) @@ -333,22 +349,11 @@ class _AddItemAction(qt.QWidgetAction): return monitorName = dialog.selectedChannelName() if monitorName is None: - return - # As we did a user selection make sure the item is still there - parentItem = self.parent().selectedPlotItem() - if parentItem is None: - return - plotModel = parentItem.plot() - if plotModel is None: - return - newItem = NormalizedCurveItem(plotModel) + return False + plotModel = item.plot() channel = plot_model.ChannelRef(plotModel, monitorName) - newItem.setMonitorChannel(channel) - newItem.setSource(parentItem) - with plotModel.transaction(): - plotModel.addItem(newItem) - # FIXME: It would be better to make it part of the model - plotModel.tagUserEditTime() + item.setMonitorChannel(channel) + return True class _DataItem(_property_tree_helper.ScanRowItem): -- GitLab