summaryrefslogtreecommitdiff
path: root/silx/gui/plot/tools/profile/ScatterProfileToolBar.py
diff options
context:
space:
mode:
Diffstat (limited to 'silx/gui/plot/tools/profile/ScatterProfileToolBar.py')
-rw-r--r--silx/gui/plot/tools/profile/ScatterProfileToolBar.py362
1 files changed, 69 insertions, 293 deletions
diff --git a/silx/gui/plot/tools/profile/ScatterProfileToolBar.py b/silx/gui/plot/tools/profile/ScatterProfileToolBar.py
index fd21515..0d30651 100644
--- a/silx/gui/plot/tools/profile/ScatterProfileToolBar.py
+++ b/silx/gui/plot/tools/profile/ScatterProfileToolBar.py
@@ -1,7 +1,7 @@
# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2018 European Synchrotron Radiation Facility
+# Copyright (c) 2018-2019 European Synchrotron Radiation Facility
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -31,196 +31,18 @@ __date__ = "28/06/2018"
import logging
-import threading
-import time
+import weakref
import numpy
-try:
- from scipy.interpolate import LinearNDInterpolator
-except ImportError:
- LinearNDInterpolator = None
-
- # Fallback using local Delaunay and matplotlib interpolator
- from silx.third_party.scipy_spatial import Delaunay
- import matplotlib.tri
-
from ._BaseProfileToolBar import _BaseProfileToolBar
-from .... import qt
from ... import items
+from ....utils.concurrent import submitToQtMainThread
_logger = logging.getLogger(__name__)
-# TODO support log scale
-
-
-class _InterpolatorInitThread(qt.QThread):
- """Thread building a scatter interpolator
-
- This works in greedy mode in that the signal is only emitted
- when no other request is pending
- """
-
- sigInterpolatorReady = qt.Signal(object)
- """Signal emitted whenever an interpolator is ready
-
- It provides a 3-tuple (points, values, interpolator)
- """
-
- _RUNNING_THREADS_TO_DELETE = []
- """Store reference of no more used threads but still running"""
-
- def __init__(self):
- super(_InterpolatorInitThread, self).__init__()
- self._lock = threading.RLock()
- self._pendingData = None
- self._firstFallbackRun = True
-
- def discard(self, obj=None):
- """Wait for pending thread to complete and delete then
-
- Connect this to the destroyed signal of widget using this thread
- """
- if self.isRunning():
- self.cancel()
- self._RUNNING_THREADS_TO_DELETE.append(self) # Keep a reference
- self.finished.connect(self.__finished)
-
- def __finished(self):
- """Handle finished signal of threads to delete"""
- try:
- self._RUNNING_THREADS_TO_DELETE.remove(self)
- except ValueError:
- _logger.warning('Finished thread no longer in reference list')
-
- def request(self, points, values):
- """Request new initialisation of interpolator
-
- :param numpy.ndarray points: Point coordinates (N, D)
- :param numpy.ndarray values: Values the N points (1D array)
- """
- with self._lock:
- # Possibly replace already pending data
- self._pendingData = points, values
-
- if not self.isRunning():
- self.start()
-
- def cancel(self):
- """Cancel any running/pending requests"""
- with self._lock:
- self._pendingData = 'cancelled'
-
- def run(self):
- """Run the init of the scatter interpolator"""
- if LinearNDInterpolator is None:
- self.run_matplotlib()
- else:
- self.run_scipy()
-
- def run_matplotlib(self):
- """Run the init of the scatter interpolator"""
- if self._firstFallbackRun:
- self._firstFallbackRun = False
- _logger.warning(
- "scipy.spatial.LinearNDInterpolator not available: "
- "Scatter plot interpolator initialisation can freeze the GUI.")
-
- while True:
- with self._lock:
- data = self._pendingData
- self._pendingData = None
-
- if data in (None, 'cancelled'):
- return
-
- points, values = data
-
- startTime = time.time()
- try:
- delaunay = Delaunay(points)
- except:
- _logger.warning(
- "Cannot triangulate scatter data")
- else:
- with self._lock:
- data = self._pendingData
-
- if data is not None: # Break point
- _logger.info('Interpolator discarded after %f s',
- time.time() - startTime)
- else:
-
- x, y = points.T
- triangulation = matplotlib.tri.Triangulation(
- x, y, triangles=delaunay.simplices)
-
- interpolator = matplotlib.tri.LinearTriInterpolator(
- triangulation, values)
-
- with self._lock:
- data = self._pendingData
-
- if data is not None:
- _logger.info('Interpolator discarded after %f s',
- time.time() - startTime)
- else:
- # No other processing requested: emit the signal
- _logger.info("Interpolator initialised in %f s",
- time.time() - startTime)
-
- # Wrap interpolator to have same API as scipy's one
- def wrapper(points):
- return interpolator(*points.T)
-
- self.sigInterpolatorReady.emit(
- (points, values, wrapper))
-
- def run_scipy(self):
- """Run the init of the scatter interpolator"""
- while True:
- with self._lock:
- data = self._pendingData
- self._pendingData = None
-
- if data in (None, 'cancelled'):
- return
-
- points, values = data
-
- startTime = time.time()
- try:
- interpolator = LinearNDInterpolator(points, values)
- except:
- _logger.warning(
- "Cannot initialise scatter profile interpolator")
- else:
- with self._lock:
- data = self._pendingData
-
- if data is not None: # Break point
- _logger.info('Interpolator discarded after %f s',
- time.time() - startTime)
- else:
- # First call takes a while, do it here
- interpolator([(0., 0.)])
-
- with self._lock:
- data = self._pendingData
-
- if data is not None:
- _logger.info('Interpolator discarded after %f s',
- time.time() - startTime)
- else:
- # No other processing requested: emit the signal
- _logger.info("Interpolator initialised in %f s",
- time.time() - startTime)
- self.sigInterpolatorReady.emit(
- (points, values, interpolator))
-
-
class ScatterProfileToolBar(_BaseProfileToolBar):
"""QToolBar providing scatter plot profiling tools
@@ -233,49 +55,13 @@ class ScatterProfileToolBar(_BaseProfileToolBar):
super(ScatterProfileToolBar, self).__init__(parent, plot, title)
self.__nPoints = 1024
- self.__interpolator = None
- self.__interpolatorCache = None # points, values, interpolator
-
- self.__initThread = _InterpolatorInitThread()
- self.destroyed.connect(self.__initThread.discard)
- self.__initThread.sigInterpolatorReady.connect(
- self.__interpolatorReady)
-
- roiManager = self._getRoiManager()
- if roiManager is None:
- _logger.error(
- "Error during scatter profile toolbar initialisation")
- else:
- roiManager.sigInteractiveModeStarted.connect(
- self.__interactionStarted)
- roiManager.sigInteractiveModeFinished.connect(
- self.__interactionFinished)
- if roiManager.isStarted():
- self.__interactionStarted(roiManager.getCurrentInteractionModeRoiClass())
-
- def __interactionStarted(self, roiClass):
- """Handle start of ROI interaction"""
- plot = self.getPlotWidget()
- if plot is None:
- return
-
- plot.sigActiveScatterChanged.connect(self.__activeScatterChanged)
-
- scatter = plot._getActiveItem(kind='scatter')
- legend = None if scatter is None else scatter.getLegend()
- self.__activeScatterChanged(None, legend)
+ self.__scatterRef = None
+ self.__futureInterpolator = None
- def __interactionFinished(self):
- """Handle end of ROI interaction"""
plot = self.getPlotWidget()
- if plot is None:
- return
-
- plot.sigActiveScatterChanged.disconnect(self.__activeScatterChanged)
-
- scatter = plot._getActiveItem(kind='scatter')
- legend = None if scatter is None else scatter.getLegend()
- self.__activeScatterChanged(legend, None)
+ if plot is not None:
+ self._setScatterItem(plot._getActiveItem(kind='scatter'))
+ plot.sigActiveScatterChanged.connect(self.__activeScatterChanged)
def __activeScatterChanged(self, previous, legend):
"""Handle change of active scatter
@@ -283,35 +69,37 @@ class ScatterProfileToolBar(_BaseProfileToolBar):
:param Union[str,None] previous:
:param Union[str,None] legend:
"""
- self.__initThread.cancel()
+ plot = self.getPlotWidget()
+ if plot is None or legend is None:
+ scatter = None
+ else:
+ scatter = plot.getScatter(legend)
+ self._setScatterItem(scatter)
- # Reset interpolator
- self.__interpolator = None
+ def _getScatterItem(self):
+ """Returns the scatter item currently handled by this tool.
- plot = self.getPlotWidget()
- if plot is None:
- _logger.error("Associated PlotWidget no longer exists")
+ :rtype: ~silx.gui.plot.items.Scatter
+ """
+ return None if self.__scatterRef is None else self.__scatterRef()
+ def _setScatterItem(self, scatter):
+ """Set the scatter tracked by this tool
+
+ :param Union[None,silx.gui.plot.items.Scatter] scatter:
+ """
+ self.__futureInterpolator = None # Reset currently expected future
+
+ previousScatter = self._getScatterItem()
+ if previousScatter is not None:
+ previousScatter.sigItemChanged.disconnect(
+ self.__scatterItemChanged)
+
+ if scatter is None:
+ self.__scatterRef = None
else:
- if previous is not None: # Disconnect signal
- scatter = plot.getScatter(previous)
- if scatter is not None:
- scatter.sigItemChanged.disconnect(
- self.__scatterItemChanged)
-
- if legend is not None:
- scatter = plot.getScatter(legend)
- if scatter is None:
- _logger.error("Cannot retrieve active scatter")
-
- else:
- scatter.sigItemChanged.connect(self.__scatterItemChanged)
- points = numpy.transpose(numpy.array((
- scatter.getXData(copy=False),
- scatter.getYData(copy=False))))
- values = scatter.getValueData(copy=False)
-
- self.__updateInterpolator(points, values)
+ self.__scatterRef = weakref.ref(scatter)
+ scatter.sigItemChanged.connect(self.__scatterItemChanged)
# Refresh profile
self.updateProfile()
@@ -322,49 +110,15 @@ class ScatterProfileToolBar(_BaseProfileToolBar):
:param ItemChangedType event:
"""
if event == items.ItemChangedType.DATA:
- self.__interpolator = None
- scatter = self.sender()
- if scatter is None:
- _logger.error("Cannot retrieve updated scatter item")
-
- else:
- points = numpy.transpose(numpy.array((
- scatter.getXData(copy=False),
- scatter.getYData(copy=False))))
- values = scatter.getValueData(copy=False)
-
- self.__updateInterpolator(points, values)
-
- # Handle interpolator init thread
-
- def __updateInterpolator(self, points, values):
- """Update used interpolator with new data"""
- if (self.__interpolatorCache is not None and
- len(points) == len(self.__interpolatorCache[0]) and
- numpy.all(numpy.equal(self.__interpolatorCache[0], points)) and
- numpy.all(numpy.equal(self.__interpolatorCache[1], values))):
- # Reuse previous interpolator
- _logger.info(
- 'Scatter changed: Reuse previous interpolator')
- self.__interpolator = self.__interpolatorCache[2]
-
- else:
- # Interpolator needs update: Start background processing
- _logger.info(
- 'Scatter changed: Rebuild interpolator')
- self.__interpolator = None
- self.__interpolatorCache = None
- self.__initThread.request(points, values)
-
- def __interpolatorReady(self, data):
- """Handle end of init interpolator thread"""
- points, values, interpolator = data
- self.__interpolator = interpolator
- self.__interpolatorCache = None if interpolator is None else data
- self.updateProfile()
+ self.updateProfile() # Refresh profile
def hasPendingOperations(self):
- return self.__initThread.isRunning()
+ """Returns True if waiting for an interpolator to be ready
+
+ :rtype: bool
+ """
+ return (self.__futureInterpolator is not None and
+ not self.__futureInterpolator.done())
# Number of points
@@ -383,8 +137,9 @@ class ScatterProfileToolBar(_BaseProfileToolBar):
npoints = int(npoints)
if npoints < 1:
raise ValueError("Unsupported number of points: %d" % npoints)
- else:
+ elif npoints != self.__nPoints:
self.__nPoints = npoints
+ self.updateProfile()
# Overridden methods
@@ -400,11 +155,16 @@ class ScatterProfileToolBar(_BaseProfileToolBar):
"""
if self.hasPendingOperations():
return 'Pre-processing data...'
-
else:
return super(ScatterProfileToolBar, self).computeProfileTitle(
x0, y0, x1, y1)
+ def __futureDone(self, future):
+ """Handle completion of the interpolator creation"""
+ if future is self.__futureInterpolator:
+ # Only handle future callbacks for the current one
+ submitToQtMainThread(self.updateProfile)
+
def computeProfile(self, x0, y0, x1, y1):
"""Compute corresponding profile
@@ -414,16 +174,32 @@ class ScatterProfileToolBar(_BaseProfileToolBar):
:param float y1: Profile end point Y coord
:return: (points, values) profile data or None
"""
- if self.__interpolator is None:
+ scatter = self._getScatterItem()
+ if scatter is None or self.hasPendingOperations():
return None
- nPoints = self.getNPoints()
+ # Lazy async request of the interpolator
+ future = scatter._getInterpolator()
+ if future is not self.__futureInterpolator:
+ # First time we request this interpolator
+ self.__futureInterpolator = future
+ if not future.done():
+ future.add_done_callback(self.__futureDone)
+ return None
+
+ if future.cancelled() or future.exception() is not None:
+ return None # Something went wrong
+ interpolator = future.result()
+ if interpolator is None:
+ return None # Cannot init an interpolator
+
+ nPoints = self.getNPoints()
points = numpy.transpose((
numpy.linspace(x0, x1, nPoints, endpoint=True),
numpy.linspace(y0, y1, nPoints, endpoint=True)))
- values = self.__interpolator(points)
+ values = interpolator(points)
if not numpy.any(numpy.isfinite(values)):
return None # Profile outside convex hull