From 67caee6994936530c184bbf621bc9e01bcceed64 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Thu, 8 Feb 2018 22:03:26 +0000 Subject: [PATCH] Optimize VectorField min distance calculation (#2314) --- holoviews/plotting/bokeh/chart.py | 3 ++- holoviews/plotting/mpl/chart.py | 13 ++--------- holoviews/plotting/util.py | 31 ++++++++++++++++++++------- tests/{ => plotting}/testplotutils.py | 21 +++++++++++++++--- 4 files changed, 45 insertions(+), 23 deletions(-) rename tests/{ => plotting}/testplotutils.py (96%) diff --git a/holoviews/plotting/bokeh/chart.py b/holoviews/plotting/bokeh/chart.py index 59e37b72fb..6e74720d06 100644 --- a/holoviews/plotting/bokeh/chart.py +++ b/holoviews/plotting/bokeh/chart.py @@ -171,17 +171,18 @@ class VectorFieldPlot(ColorbarPlot): def _get_lengths(self, element, ranges): mag_dim = element.get_dimension(self.size_index) (x0, x1), (y0, y1) = (element.range(i) for i in range(2)) - base_dist = get_min_distance(element) if mag_dim: magnitudes = element.dimension_values(mag_dim) _, max_magnitude = ranges[mag_dim.name] if self.normalize_lengths and max_magnitude != 0: magnitudes = magnitudes / max_magnitude if self.rescale_lengths: + base_dist = get_min_distance(element) magnitudes *= base_dist else: magnitudes = np.ones(len(element)) if self.rescale_lengths: + base_dist = get_min_distance(element) magnitudes *= base_dist return magnitudes diff --git a/holoviews/plotting/mpl/chart.py b/holoviews/plotting/mpl/chart.py index 4b4d333c22..30acebcc0e 100644 --- a/holoviews/plotting/mpl/chart.py +++ b/holoviews/plotting/mpl/chart.py @@ -605,16 +605,6 @@ class VectorFieldPlot(ColorbarPlot): _plot_methods = dict(single='quiver') - def __init__(self, *args, **params): - super(VectorFieldPlot, self).__init__(*args, **params) - self._min_dist = self._get_map_info(self.hmap) - - - def _get_map_info(self, vmap): - """ - Get the minimum sample distance and maximum magnitude - """ - return np.min([get_min_distance(vfield) for vfield in vmap]) def get_data(self, element, ranges, style): @@ -627,7 +617,8 @@ def get_data(self, element, ranges, style): if self.invert_axes: radians = radians+1.5*np.pi angles = list(np.rad2deg(radians)) if self.rescale_lengths: - input_scale = input_scale / self._min_dist + min_dist = get_min_distance(element) + input_scale = input_scale / min_dist mag_dim = element.get_dimension(self.size_index) if mag_dim: diff --git a/holoviews/plotting/util.py b/holoviews/plotting/util.py index a8cd828a34..66c12eefa5 100644 --- a/holoviews/plotting/util.py +++ b/holoviews/plotting/util.py @@ -1,6 +1,8 @@ from __future__ import unicode_literals, absolute_import + from collections import defaultdict import traceback +import warnings import numpy as np import param @@ -549,19 +551,32 @@ def traverse_setter(obj, attribute, value): obj.traverse(lambda x: setattr(x, attribute, value)) +def _get_min_distance_numpy(element): + """ + NumPy based implementation of get_min_distance + """ + xys = element.array([0, 1]) + with warnings.catch_warnings(): + warnings.filterwarnings('ignore', r'invalid value encountered in') + xys = xys.astype('float32').view(np.complex64) + distances = np.abs(xys.T-xys) + np.fill_diagonal(distances, np.inf) + distances = distances[distances>0] + if len(distances): + return distances.min() + return 0 + + def get_min_distance(element): """ Gets the minimum sampling distance of the x- and y-coordinates in a grid. """ - xys = element.array([0, 1]).astype('float64').view(dtype=np.complex128) - m, n = np.meshgrid(xys, xys) - distances = np.abs(m-n) - np.fill_diagonal(distances, np.inf) - distances = distances[distances>0] - if len(distances): - return distances.min() - return 0 + try: + from scipy.spatial.distance import pdist + return pdist(element.array([0, 1])).min() + except: + return _get_min_distance_numpy(element) def rgb2hex(rgb): diff --git a/tests/testplotutils.py b/tests/plotting/testplotutils.py similarity index 96% rename from tests/testplotutils.py rename to tests/plotting/testplotutils.py index 0e9a886049..20d6de3ec0 100644 --- a/tests/testplotutils.py +++ b/tests/plotting/testplotutils.py @@ -1,3 +1,5 @@ +from __future__ import absolute_import + from unittest import SkipTest from nose.plugins.attrib import attr @@ -12,7 +14,7 @@ from holoviews.operation import operation from holoviews.plotting.util import ( compute_overlayable_zorders, get_min_distance, process_cmap, - initialize_dynamic, split_dmap_overlay) + initialize_dynamic, split_dmap_overlay, _get_min_distance_numpy) from holoviews.streams import PointerX try: @@ -430,13 +432,12 @@ class TestPlotColorUtils(ComparisonTestCase): def test_process_cmap_mpl(self): colors = process_cmap('Greys', 3) self.assertEqual(colors, ['#ffffff', '#959595', '#000000']) - def test_process_cmap_instance_mpl(self): try: from matplotlib.cm import get_cmap except: - raise SkipError("Matplotlib needed to test matplotlib colormap instances") + raise SkipTest("Matplotlib needed to test matplotlib colormap instances") cmap = get_cmap('Greys') colors = process_cmap(cmap, 3) self.assertEqual(colors, ['#ffffff', '#959595', '#000000']) @@ -478,6 +479,20 @@ def test_get_min_distance_int32_type(self): dist = get_min_distance(Points((X.flatten(), Y.flatten()))) self.assertEqual(dist, 1.0) + def test_get_min_distance_float32_type_no_scipy(self): + xs, ys = (np.arange(0, 2., .2, dtype='float32'), + np.arange(0, 2., .2, dtype='float32')) + X, Y = np.meshgrid(xs, ys) + dist = _get_min_distance_numpy(Points((X.flatten(), Y.flatten()))) + self.assertEqual(dist, np.float32(0.2)) + + def test_get_min_distance_int32_type_no_scipy(self): + xs, ys = (np.arange(0, 10, dtype='int32'), + np.arange(0, 10, dtype='int32')) + X, Y = np.meshgrid(xs, ys) + dist = _get_min_distance_numpy(Points((X.flatten(), Y.flatten()))) + self.assertEqual(dist, 1.0) + @attr(optional=1) # Flexx is optional class TestBokehUtils(ComparisonTestCase):