Commit 6f34f07d authored by Cyril Guilloud's avatar Cyril Guilloud
Browse files

Merge branch 'flint-window-average' into 'master'

Flint: Mean filter on sliding window

Closes #3371

See merge request !4673
parents 0adfae1e 1caacdb4
Pipeline #75326 passed with stages
in 165 minutes and 31 seconds
......@@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Flint
- Added widget in the regulation plot to specify a time to life for the data
- Added sliding window mean filter to the live curve plot
### Changed
......
# -*- 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 mean filter based on a sliding window.
"""
from __future__ import annotations
from typing import Optional
from typing import NamedTuple
from typing import Dict
from typing import Any
import numpy
from numpy.lib.stride_tricks import sliding_window_view
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 SlidingWindowData(NamedTuple):
xx: numpy.ndarray
yy: numpy.ndarray
nb_points: int
class SlidingWindowMeanItem(ComputedCurveItem, plot_model.IncrementalComputableMixIn):
"""This item use the scan data to process result before displaying it."""
NAME = "Sliding window mean"
ICON_NAME = "flint:icons/item-func"
DEFAULT_EXTRA_POINTS = 5
"""Extra points used on each side of the actual position"""
def __init__(self, parent=None):
ComputedCurveItem.__init__(self, parent=parent)
plot_model.IncrementalComputableMixIn.__init__(self)
self.__extra_points = self.DEFAULT_EXTRA_POINTS
def name(self) -> str:
nb = self.__extra_points * 2 + 1
return f"MeanOnWindow{nb}"
def setHalfWindowSize(self, nbPoints: int):
"""Set the number of point for the half window.
- `0` means the mean is computed with a single point.
- `5` means the mean is computed with 5+1+5 points
"""
self.__extra_points = nbPoints
def halfWindowSize(self) -> int:
return self.__extra_points
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[SlidingWindowData]:
sourceItem = self.source()
xx = sourceItem.xArray(scan)
yy = sourceItem.yArray(scan)
if xx is None or yy is None:
return None
window_size = self.__extra_points * 2 + 1
nb_points = min(len(xx), len(yy))
if nb_points < window_size:
return None
xx, yy = xx[:nb_points], yy[:nb_points]
xwindow = xx[self.__extra_points : -self.__extra_points]
ywindow = sliding_window_view(yy, window_size)
ywindow = numpy.mean(ywindow, axis=1)
return SlidingWindowData(xwindow, ywindow, nb_points)
def incrementalCompute(
self, previousResult: SlidingWindowData, scan: scan_model.Scan
) -> SlidingWindowData:
"""Compute a data using the previous value as basis.
The function expect extra points before and after the
each points it can compute.
"""
if previousResult is None:
return self.compute(scan)
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_points = min(len(xx), len(yy))
if nb_points <= previousResult.nb_points:
# Obviously nothing to compute
return previousResult
window_size = self.__extra_points * 2 + 1
nb_points = min(len(xx), len(yy))
if nb_points < window_size:
# There is still not enough data
return SlidingWindowData(previousResult.xx, previousResult.yy, nb_points)
# There is necessary something new to compute
start = len(previousResult.xx) - self.__extra_points
xx, yy = xx[:nb_points], yy[start:nb_points]
xwindow = xx[self.__extra_points : -self.__extra_points]
ywindow = sliding_window_view(yy, window_size)
ywindow = numpy.mean(ywindow, axis=1)
ywindow = numpy.append(previousResult.yy, ywindow)
return SlidingWindowData(xwindow, ywindow, nb_points)
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 "mean(%s)" % sourceItem.displayName("y", scan)
else:
assert False
......@@ -36,6 +36,7 @@ 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 bliss.flint.filters.sliding_window_mean import SlidingWindowMeanItem
from . import delegates
from . import data_views
from . import _property_tree_helper
......@@ -278,6 +279,7 @@ class _AddItemAction(qt.QWidgetAction):
GaussianFitItem: None,
NormalizedCurveItem: self.__editNormalizedParams,
NormalizedZeroOneItem: None,
SlidingWindowMeanItem: self.__editSlidingWindowMeanParams,
}
item = self.parent().selectedPlotItem()
......@@ -355,6 +357,27 @@ class _AddItemAction(qt.QWidgetAction):
item.setMonitorChannel(channel)
return True
def __editSlidingWindowMeanParams(self, item: SlidingWindowMeanItem):
"""Edit SlidingWindowMeanItem parameters.
Returns true if the edition succeeded, else false.
"""
parentWidget = self.parent()
value = item.halfWindowSize() * 2 + 1
value, result = qt.QInputDialog.getInt(
parentWidget,
"Edit sliding window",
"Nb elements for the window",
value,
1,
100,
)
if not result:
return False
nbPoints = (value - 1) // 2
item.setHalfWindowSize(nbPoints)
return True
class _DataItem(_property_tree_helper.ScanRowItem):
......
......@@ -18,7 +18,7 @@ This scans have to be created usually per beamline in a very specific way.
For this examples we will use a function to simulate them.
```
```python
import gevent
import numpy
from bliss.scanning.group import Sequence
......@@ -53,7 +53,7 @@ This example create a regular scatter.
![Regular scatter in Flint](img/scan_info/regular-scatter.png)
```
```python
from bliss.scanning.scan_info import ScanInfo
import numpy
......@@ -109,7 +109,7 @@ For now only Flint will only display the last frame.
![Regular scatter in Flint](img/scan_info/regular-scatter-3d.png)
```
```python
from bliss.scanning.scan_info import ScanInfo
import numpy
......@@ -157,7 +157,7 @@ That's what we simulate here. `axis1` and `axis2` are also provided as
![Axis and encoder together](img/scan_info/scatter-encoder.png)
```
```python
from bliss.scanning.scan_info import ScanInfo
import numpy
......@@ -217,7 +217,7 @@ used to know the amount of pixels to used per axis.
![Non-regular scatter in Flint](img/scan_info/irregular-scatter.png)
```
```python
from bliss.scanning.scan_info import ScanInfo
import numpy
......@@ -255,7 +255,7 @@ This also can work for extra dimensions with steppers.
![Non-regular scatter in Flint](img/scan_info/irregular-scatter-3d.png)
```
```python
from bliss.scanning.scan_info import ScanInfo
import numpy
......
......@@ -8,7 +8,7 @@ with the data to be displayed.
## Basic plot display
A generic display is provided through the BLISS `plot` command.
A generic display is provided through the BLISS `plot()` command.
It checks the data dimensionality to select the kind of charts.
......@@ -40,7 +40,7 @@ plot(cube)
To use more features another way is provided.
First a plot have to be created from Flint.
First a plot has to be created from Flint.
The first argument (here `curve`) is used to select the kind of expected plot.
See the following documentation for example of each kind.
......@@ -384,7 +384,7 @@ p.export_to_logbook()
From a scripts Flint also can be used to display plot.
The `plot` command can be imported the following way:
The `plot()` command can be imported the following way:
```python
from bliss.common.plot import plot
......
......@@ -55,7 +55,7 @@ From BLISS shell, few basic interaction with Flint are provided.
To invoke or retrieve a proxy to interact with Flint you can use:
```
```python
flint()
```
......@@ -65,7 +65,7 @@ flint()
The returned object provides few methods to close the application:
```
```python
f = flint()
f.close() # request a normal close of the application
f.kill() # kill
......@@ -77,7 +77,7 @@ f.kill9() # kill -9
If your window manager provides this feature, you can force the focus on the
application this way:
```
```python
f = flint()
f.focus()
```
......
......@@ -4,6 +4,33 @@ If the scan contains counters, the curve widget will be displayed automatically.
![Flint screenshot](img/flint-curve-widget.png)
## Statistic tools
Some statistics can be added to the graph as curve or markers.
By clicking on the "Create new items in the plot" ![Flint
screenshot](img/curve_icon_add_curve.png) icon, a new curve or marker can be
added to represent a calculation based on the current curve.
The statistical filters available are:
* Max marker
* Min marker
* Derivative function
* Negative function
* Gaussian fit
* Normalized function
* Normalized range
* Sliding windows mean
For example, on this plot, min/max markers and a 11-points sliding window mean
have been added.
![Curve statistics](img/curve_statistics.png)
## Compare scans
A tool provided in the property tool bar allow to compare scans.
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment