Commit 0f9b42d4 authored by Matias Guijarro's avatar Matias Guijarro
Browse files

Merge branch 'compare-curves' into 'master'

Flint: Display curves from many scans

Closes #1936

See merge request !3771
parents 1d95074f 687596f0
Pipeline #48631 passed with stages
in 129 minutes and 56 seconds
......@@ -280,6 +280,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added
- Flint
- Added a tool to display many scans in the curve widget
### Changed
### Fixed
......
......@@ -15,6 +15,7 @@ from typing import List
from typing import Dict
from typing import Tuple
import logging
from bliss.flint.model import scan_model
from bliss.flint.model import flint_model
from bliss.flint.model import plot_model
......@@ -22,6 +23,9 @@ from bliss.flint.model import plot_item_model
from bliss.flint.model import plot_state_model
_logger = logging.getLogger(__name__)
class DefaultStyleStrategy(plot_model.StyleStrategy):
def __init__(self, flintModel: flint_model.FlintState = None):
super(DefaultStyleStrategy, self).__init__()
......@@ -30,10 +34,16 @@ class DefaultStyleStrategy(plot_model.StyleStrategy):
Tuple[plot_model.Item, Optional[scan_model.Scan]], plot_model.Style
] = {}
self.__cacheInvalidated = True
self.__scans = []
def setFlintModel(self, flintModel: flint_model.FlintState):
self.__flintModel = flintModel
def setScans(self, scans):
self.__scans.clear()
self.__scans.extend(scans)
self.invalidateStyles()
def __getstate__(self):
return {}
......@@ -119,37 +129,90 @@ class DefaultStyleStrategy(plot_model.StyleStrategy):
self.cacheStyle(scatter, None, style)
def computeItemStyleFromCurvePlot(self, plot, scans):
i = 0
countBase = 0
if len(scans) == 1:
countBase = 0
for item in plot.items():
if not isinstance(item, plot_model.ComputableMixIn):
countBase += 1
for item in plot.items():
if isinstance(item, plot_item_model.ScanItem):
pass
elif isinstance(item, plot_model.ComputableMixIn):
pass
else:
# That's a main item
countBase += 1
if len(scans) <= 1:
if countBase <= 1:
self.computeItemStyleFromCurvePlot_eachItemsColored(plot, scans)
else:
self.computeItemStyleFromCurvePlot_firstScanColored(plot, scans)
else:
if countBase > 1:
self.computeItemStyleFromCurvePlot_firstScanColored(plot, scans)
else:
self.computeItemStyleFromCurvePlot_eachScanColored(plot, scans)
def computeItemStyleFromCurvePlot_eachItemsColored(self, plot, scans):
i = 0
for scan in scans:
for item in plot.items():
if isinstance(item, plot_item_model.ScanItem):
continue
if isinstance(item, plot_model.ComputableMixIn):
if countBase == 1:
# Allocate a new color for everything
# Allocate a new color for everything
color = self.pickColor(i)
i += 1
if isinstance(item, plot_state_model.CurveStatisticItem):
style = plot_model.Style(lineStyle=":", lineColor=color)
else:
style = plot_model.Style(lineStyle="-.", lineColor=color)
else:
color = self.pickColor(i)
style = plot_model.Style(lineStyle="-", lineColor=color)
i += 1
self.cacheStyle(item, scan, style)
def computeItemStyleFromCurvePlot_firstScanColored(self, plot, scans):
i = 0
for scanId, scan in enumerate(scans):
for item in plot.items():
if isinstance(item, plot_item_model.ScanItem):
continue
if isinstance(item, plot_model.ComputableMixIn):
# Reuse the parent color
source = item.source()
baseStyle = self.getStyleFromItem(source, scan)
color = baseStyle.lineColor
if isinstance(item, plot_state_model.CurveStatisticItem):
style = plot_model.Style(lineStyle=":", lineColor=color)
else:
style = plot_model.Style(lineStyle="-.", lineColor=color)
else:
if scanId == 0:
color = self.pickColor(i)
i += 1
else:
# Reuse the color
source = item.source()
baseStyle = self.getStyleFromItem(source, scan)
color = baseStyle.lineColor
# Grayed
color = (0x80, 0x80, 0x80)
style = plot_model.Style(lineStyle="-", lineColor=color)
self.cacheStyle(item, scan, style)
def computeItemStyleFromCurvePlot_eachScanColored(self, plot, scans):
for scanId, scan in enumerate(scans):
for item in plot.items():
if isinstance(item, plot_item_model.ScanItem):
continue
if isinstance(item, plot_model.ComputableMixIn):
# Reuse the parent color
source = item.source()
baseStyle = self.getStyleFromItem(source, scan)
color = baseStyle.lineColor
if isinstance(item, plot_state_model.CurveStatisticItem):
style = plot_model.Style(lineStyle=":", lineColor=color)
else:
style = plot_model.Style(lineStyle="-.", lineColor=color)
else:
color = self.pickColor(i)
color = self.pickColor(scanId)
style = plot_model.Style(lineStyle="-", lineColor=color)
i += 1
self.cacheStyle(item, scan, style)
def computeItemStyleFromPlot(self):
......@@ -161,11 +224,14 @@ class DefaultStyleStrategy(plot_model.StyleStrategy):
self.computeItemStyleFromImagePlot(plot)
else:
scans: List[Optional[scan_model.Scan]] = []
for item in plot.items():
if isinstance(item, plot_item_model.ScanItem):
scans.append(item.scan())
if scans == []:
scans.append(None)
if len(self.__scans) > 0:
scans = self.__scans
else:
for item in plot.items():
if isinstance(item, plot_item_model.ScanItem):
scans.append(item.scan())
if scans == []:
scans.append(None)
self.computeItemStyleFromCurvePlot(plot, scans)
......
......@@ -148,6 +148,21 @@ class CurveItem(plot_model.Item, CurveMixIn):
def isValid(self):
return self.__x is not None and self.__y is not None
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 self.isValid():
return False
channel = self.xChannel()
if channel.channel(scan) is None:
return False
channel = self.yChannel()
if channel.channel(scan) is None:
return False
return True
def getScanValidation(self, scan: scan_model.Scan) -> Optional[str]:
"""
Returns None if everything is fine, else a message to explain the problem.
......@@ -229,6 +244,18 @@ class XIndexCurveItem(CurveItem):
channel = self.yChannel()
return channel is not None
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 self.isValid():
return False
channel = self.yChannel()
if channel.channel(scan) is None:
return False
return True
def xData(self, scan: scan_model.Scan) -> Optional[scan_model.Data]:
yData = self.yData(scan)
if yData is None:
......
......@@ -381,6 +381,13 @@ class Item(qt.QObject):
"""
return None
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.
"""
return True
def isValidInScan(self, scan: scan_model.Scan) -> bool:
"""Returns true if this item do not have any messages associated with
the data of this scan."""
......@@ -511,6 +518,19 @@ class ChildItem(Item):
def source(self) -> Optional[Item]:
return self.__source
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 self.isValid():
return False
source = self.source()
if source is not None:
if not source.isAvailableInScan(scan):
return False
return True
class ComputableMixIn:
"""This item use the scan data to process result before displaying it."""
......
......@@ -595,6 +595,19 @@ class NormalizedCurveItem(plot_model.ChildItem, plot_item_model.CurveMixIn):
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()
......
......@@ -394,6 +394,10 @@ class Scan(qt.QObject, _Sealable):
raise KeyError("Version do not match")
return result[1]
def startTime(self):
scanInfo = self.scanInfo()
return scanInfo.get("start_time", None)
class ScanGroup(Scan):
"""Scan group object.
......
<?xml version="1.0" encoding="UTF-8"?>
<svg id="svg12" enable-background="new 0 0 32 32" version="1.1" viewBox="0 0 32 32" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"><metadata id="metadata2"><rdf:RDF><cc:Work rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/><dc:title/></cc:Work></rdf:RDF></metadata><path id="path16-3" d="m30.24 26.443c-0.2099-0.02224-13.302-0.01495-14.181-0.24464-1.4684-0.38394-1.4525-0.21784-2.0548-1.3356-1.084-2.0116 0.06041-19.343-4.7282-19.387-4.7886-0.044-3.6546 18.21-5.6019 19.861-1.9473 1.6509-1.9292 1.1062-1.9292 1.1062" fill="none" stroke="#00a14b" stroke-linecap="round" stroke-width="2.109"/><path id="path16-3-5-5" d="m28.024 23.52c-1.084-2.0116-0.05811-18-4.8468-18.044-0.84925-0.00778-1.5122 0.55994-2.0391 1.5085" fill="none" stroke="#758c80" stroke-linecap="round" stroke-width="2.109"/><path id="path16-3-5-5-1" d="m21.111 23.52c-1.084-2.0116-0.05811-18-4.8468-18.044-0.84925-0.00778-1.5122 0.55994-2.0391 1.5085" fill="none" stroke="#758c80" stroke-linecap="round" stroke-width="2.109"/></svg>
......@@ -119,9 +119,17 @@ class CurvePlotWidget(plot_helper.PlotWidget):
plotItemSelected = qt.Signal(object)
"""Emitted when a flint plot item was selected by the plot"""
scanSelected = qt.Signal(object)
"""Emitted when a flint plot item was selected by the plot"""
scanListUpdated = qt.Signal(object)
"""Emitted when the list of scans is changed"""
def __init__(self, parent=None):
super(CurvePlotWidget, self).__init__(parent=parent)
self.__scan: Optional[scan_model.Scan] = None
self.__scans: List[scan_model.Scan] = []
self.__maxStoredScans = 3
self.__storePreviousScans = False
self.__flintModel: Optional[flint_model.FlintState] = None
self.__plotModel: plot_model.Plot = None
......@@ -144,6 +152,7 @@ class CurvePlotWidget(plot_helper.PlotWidget):
self.__plot.setBackgroundColor("white")
self.__view = view_helper.ViewManager(self.__plot)
self.__selectedPlotItem = None
self.__selectedScan: Optional[scan_model.Scan] = None
self.__aggregator = plot_helper.ScalarEventAggregator(self)
self.__refreshManager = refresh_helper.RefreshManager(self)
......@@ -299,56 +308,78 @@ class CurvePlotWidget(plot_helper.PlotWidget):
propertyWidget = curve_plot_property.CurvePlotPropertyWidget(parent)
propertyWidget.setFlintModel(self.__flintModel)
propertyWidget.setFocusWidget(self)
propertyWidget.plotItemSelected.connect(self.__plotItemSelectedFromProperty)
return propertyWidget
def __findItemFromPlotItem(self, requestedItem: plot_model.Item):
"""Returns a silx plot item from a flint plot item."""
def __findItemFromPlot(
self, requestedItem: plot_model.Item, requestedScan: scan_model.Scan
):
"""Returns a silx plot item from a flint plot item and scan."""
if requestedItem is None:
return None
alternative = None
for item in self.__plot.getItems():
if isinstance(item, plot_helper.FlintCurve):
plotItem = item.customItem()
if plotItem is requestedItem:
return item
return None
if item.customItem() is not requestedItem:
continue
if item.scan() is not requestedScan:
if item.scan() is self.__scan:
alternative = item
continue
return item
return alternative
def selectedPlotItem(self) -> Optional[plot_model.Item]:
"""Returns the current selected plot item, if one"""
return self.__selectedPlotItem
def selectedScan(self) -> Optional[scan_model.Scan]:
"""Returns the current selected scan, if one"""
return self.__selectedScan
def __selectionChanged(self, previous, current):
"""Callback executed when the selection from the plot was changed"""
if isinstance(current, plot_helper.FlintCurve):
selected = current.customItem()
scanSelected = current.scan()
else:
selected = None
scanSelected = None
self.__selectedPlotItem = selected
self.plotItemSelected.emit(selected)
self.scanSelected.emit(scanSelected)
if self.__specMode.isEnabled():
self.__updateTitle(self.__scan)
def __plotItemSelectedFromProperty(self, selected):
"""Callback executed when the selection from the property view was
changed"""
self.selectPlotItem(selected)
def selectScan(self, select: scan_model.Scan):
wasUpdated = self.__selectedScan is not select
self.__selectedScan = select
if wasUpdated:
self.scanSelected.emit(select)
self.__updatePlotWithSelectedCurve()
def selectPlotItem(self, selected: plot_model.Item, force=False):
def selectPlotItem(self, select: plot_model.Item, force=False):
"""Select a flint plot item"""
if not force:
if self.__selectedPlotItem is selected:
if self.__selectedPlotItem is select:
return
if selected is self.selectedPlotItem():
if select is self.selectedPlotItem():
# Break reentrant signals
return
self.__selectedPlotItem = selected
item = self.__findItemFromPlotItem(selected)
wasUpdated = self.__selectedPlotItem is not select
self.__selectedPlotItem = select
if wasUpdated:
self.plotItemSelected.emit(select)
self.__updatePlotWithSelectedCurve()
def __updatePlotWithSelectedCurve(self):
item = self.__findItemFromPlot(self.__selectedPlotItem, self.__selectedScan)
# FIXME: We should not use the legend
if item is None:
legend = None
else:
legend = item.getLegend()
self.__plot.setActiveCurve(legend)
with qtutils.blockSignals(self.__plot):
self.__plot.setActiveCurve(legend)
def flintModel(self) -> Optional[flint_model.FlintState]:
return self.__flintModel
......@@ -368,7 +399,9 @@ class CurvePlotWidget(plot_helper.PlotWidget):
self.__plotModel.transactionFinished.disconnect(
self.__aggregator.callbackTo(self.__transactionFinished)
)
previousModel = self.__plotModel
self.__plotModel = plotModel
self.__syncStyleStrategy()
if self.__plotModel is not None:
self.__plotModel.structureChanged.connect(
self.__aggregator.callbackTo(self.__structureChanged)
......@@ -380,9 +413,28 @@ class CurvePlotWidget(plot_helper.PlotWidget):
self.__aggregator.callbackTo(self.__transactionFinished)
)
self.plotModelUpdated.emit(plotModel)
self.__reselectPlotItem(previousModel, plotModel)
self.__redrawAllScans()
self.__syncAxisTitle.trigger()
def __reselectPlotItem(self, previousModel, plotModel):
"""Update the plot item selection from the previous plot model to the
new plot model"""
if previousModel is None or plotModel is None:
return
selectedItem = self.__selectedPlotItem
if selectedItem is None:
return
expectedLabel = selectedItem.displayName("y", scan=None)
for item in plotModel.items():
if isinstance(item, plot_item_model.CurveMixIn):
if item.isValid():
label = item.displayName("y", scan=None)
if label == expectedLabel:
self.selectPlotItem(item)
return
self.selectPlotItem(None)
def plotModel(self) -> plot_model.Plot:
return self.__plotModel
......@@ -535,6 +587,53 @@ class CurvePlotWidget(plot_helper.PlotWidget):
self.__boundingY2.setRange(xMin, xMax)
self.__boundingY2.setVisible(True)
def scanList(self):
return self.__scans
def removeScan(self, scan):
if scan is None:
return
if scan is self.__scan:
_logger.warning("Removing the current scan is not available")
return
self.__scans.remove(scan)
self.__syncStyleStrategy()
self.scanListUpdated.emit(self.__scans)
self.__redrawAllScans()
def insertScan(self, scan):
if scan is None:
return
if scan is self.__scan:
_logger.warning("Removing the current scan is not available")
return
self.__scans.append(scan)
self.__scans = list(reversed(sorted(self.__scans, key=lambda s: s.startTime())))
self.__syncStyleStrategy()
self.scanListUpdated.emit(self.__scans)
self.__redrawAllScans()
def setMaxStoredScans(self, maxScans: int):
# FIXME: Must emit event
self.__maxStoredScans = maxScans
def maxStoredScans(self) -> int:
return self.__maxStoredScans
def setPreviousScanStored(self, storeScans: bool):
# FIXME: Must emit event
self.__storePreviousScans = storeScans
def isPreviousScanStored(self) -> bool:
return self.__storePreviousScans
@property
def __scan(self):
if len(self.__scans) == 0:
return None
else:
return self.__scans[0]
def scan(self) -> Optional[scan_model.Scan]:
return self.__scan
......@@ -551,7 +650,19 @@ class CurvePlotWidget(plot_helper.PlotWidget):
self.__scan.scanFinished.disconnect(
self.__aggregator.callbackTo(self.__scanFinished)
)
self.__scan = scan
if self.__storePreviousScans:
if scan is not None:
self.__scans.insert(0, scan)
while len(self.__scans) > self.__maxStoredScans:
del self.__scans[-1]
else:
if scan is not None:
self.__scans.clear()
self.__scans.append(scan)
self.__syncStyleStrategy()
self.scanListUpdated.emit(self.__scans)
self.__selectedScan = self.__scan
self.scanSelected.emit(self.__scan)
if self.__scan is not None:
self.__scan.scanDataUpdated[object].connect(
self.__aggregator.callbackTo(self.__scanDataUpdated)
......@@ -569,6 +680,12 @@ class CurvePlotWidget(plot_helper.PlotWidget):
self.__redrawAllScans()
self.__syncAxisTitle.trigger()
def __syncStyleStrategy(self):
if self.__plotModel is not None:
styleStrategy = self.__plotModel.styleStrategy()
if styleStrategy is not None:
styleStrategy.setScans(self.__scans)
def __cleanScanIfNeeded(self, scan):
plotModel = self.__plotModel
if plotModel is None:
......@@ -648,9 +765,10 @@ class CurvePlotWidget(plot_helper.PlotWidget):
for scan in scanItems:
self.__redrawScan(scan.scan())
else:
currentScan = self.__scan
if currentScan is not None:
self.__redrawScan(currentScan)
for s in self.__scans:
if s is None:
continue
self.__redrawScan(s)
def __cleanScan(self, scan: scan_model.Scan):
items = self.__items.pop(scan, {})
......@@ -714,10 +832,10 @@ class CurvePlotWidget(plot_helper.PlotWidget):
for scan in scanItems:
self.__updatePlotItem(item, scan.scan())
else:
currentScan = self.__scan
if currentScan is None:
return
self.__updatePlotItem(item, currentScan)
for s in self.__scans:
if s is None:
continue
self.__updatePlotItem(item, s)
if reselect is not None:
self.selectPlotItem(reselect)
......@@ -767,6 +885,7 @@ class CurvePlotWidget(plot_helper.PlotWidget):
style = item.getStyle(scan)
curveItem = plot_helper.FlintCurve()
curveItem.setCustomItem(item)
curveItem.setScan(scan)
curveItem.setData(x=xx, y=yy, copy=False)
curveItem.setName(legend)
curveItem.setLineStyle(style.lineStyle)
......
......@@ -10,6 +10,7 @@ from typing import Union
from typing import List
from typing import Dict
from typing import Optional
from typing import NamedTuple
import logging
import functools
......@@ -29,6 +30,7 @@ from bliss.flint.helper.style_helper import DefaultStyleStrategy
from bliss.flint.utils import qmodelutils
from bliss.flint.widgets.select_channel_dialog import SelectChannelDialog
from . import delegates
from . import data_views
from . import _property_tree_helper
......@@ -637,6 +639,92 @@ class _DataItem(_property_tree_helper.ScanRowItem):
self.updateError()
class ScanItem(NamedTuple):
scan: scan_model.Scan
plotModel: plot_model.Plot
plotItem: plot_model.Item
curveWidget: qt.QWidget
class ScanTableView(data_views.VDataTableView):
ScanNbColumn = 0
ScanTitleColumn = 1
ScanStartTimeColumn = 2