diff options
Diffstat (limited to 'silx/gui/plot/tools/profile/ScatterProfileToolBar.py')
-rw-r--r-- | silx/gui/plot/tools/profile/ScatterProfileToolBar.py | 362 |
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 |