From 755fa4ad5bf0be4a75ae53ba8a92ad44bf4f8380 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Fri, 5 Jan 2024 19:31:16 +0100 Subject: [PATCH 1/4] Use tsdownsample library for downsampling if available --- holoviews/operation/downsample.py | 83 +++++++++++++++++++++++++++---- 1 file changed, 74 insertions(+), 9 deletions(-) diff --git a/holoviews/operation/downsample.py b/holoviews/operation/downsample.py index f7b2e75c82..c85690c669 100644 --- a/holoviews/operation/downsample.py +++ b/holoviews/operation/downsample.py @@ -41,6 +41,7 @@ def _argmax_area(prev_x, prev_y, avg_next_x, avg_next_y, x_bucket, y_bucket): """Vectorized triangular area argmax computation. + Parameters ---------- prev_x : float @@ -99,7 +100,7 @@ def _lttb_inner(x, y, n_out, sampled_x, offset): ) -def _lttb(x, y, n_out): +def _lttb(x, y, n_out, **kwargs): """ Downsample the data using the LTTB algorithm (python implementation). @@ -110,6 +111,14 @@ def _lttb(x, y, n_out): Returns: np.array: The indexes of the selected datapoints. """ + try: + from tsdownsample import LTTBDownsampler + return LTTBDownsampler().downsample(x, y, n_out=n_out, **kwargs) + except Exception as e: + raise e + except ModuleNotFoundError: + pass + # Bucket size. Leave room for start and end data points block_size = (y.shape[0] - 2) / (n_out - 2) # Note this 'astype' cast must take place after array creation (and not with the @@ -132,8 +141,7 @@ def _lttb(x, y, n_out): return sampled_x - -def _nth_point(x, y, n_out): +def _nth_point(x, y, n_out, **kwargs): """ Downsampling by selecting every n-th datapoint @@ -147,24 +155,78 @@ def _nth_point(x, y, n_out): n_samples = len(x) return np.arange(0, n_samples, max(1, math.ceil(n_samples / n_out))) +def _min_max(x, y, n_out, **kwargs): + try: + from tsdownsample import MinMaxDownsampler + except Exception as e: + raise e + except ModuleNotFoundError as e: + raise NotImplementedError( + 'The min-max downsampling algorithm requires the tsdownsampler ' + 'library to be installed.' + ) from e + return MinMaxDownsampler().downsample(x, y, n_out=n_out, **kwargs) + +def _min_max_lttb(x, y, n_out, **kwargs): + try: + from tsdownsample import MinMaxLTTBDownsampler + except Exception as e: + raise e + except ModuleNotFoundError as e: + raise NotImplementedError( + 'The minmax-lttb downsampling algorithm requires the tsdownsampler ' + 'library to be installed.' + ) from e + return MinMaxLTTBDownsampler().downsample(x, y, n_out=n_out, **kwargs) + +def _m4(x, y, n_out, **kwargs): + try: + from tsdownsample import M4Downsampler + except Exception as e: + raise e + except ModuleNotFoundError as e: + raise NotImplementedError( + 'The m4 downsampling algorithm requires the tsdownsampler ' + 'library to be installed.' + ) from e + return M4Downsampler().downsample(x, y, n_out=n_out, **kwargs) + _ALGORITHMS = { 'lttb': _lttb, - 'nth': _nth_point -} + 'nth': _nth_point, + 'minmax': _min_max, + 'minmax-lttb': _min_max_lttb, + 'm4': _m4, +} class downsample1d(ResampleOperation1D): """ Implements downsampling of a regularly sampled 1D dataset. - Supports multiple algorithms: + If available uses the `tsdownsampler` library to perform massively + accelerated downsampling. + """ + + algorithm = param.Selector(default='lttb', objects=list(_ALGORITHMS), doc=""" + The algorithm to use for downsampling: - `lttb`: Largest Triangle Three Buckets downsample algorithm - `nth`: Selects every n-th point. - """ + - `minmax`: Selects the min and max value in each bin (requires tsdownsampler). + - `m4`: Selects the min, max, first and last value in each bin (requires tsdownsampler). + - `minmax-lttb`: First selects n_out * minmax_ratio min and max values, + then further reduces these to n_out values using the + Largest Triangle Three Buckets algorithm. (requires tsdownsampler)""") + + parallel = param.Boolean(default=False, doc=""" + The number of threads to use (if tsdownsampler is available).""") - algorithm = param.Selector(default='lttb', objects=['lttb', 'nth']) + minmax_ratio = param.Integer(default=4, bounds=(0, None), doc=""" + For the minmax-lttb algorithm determines the ratio of candidate + values to generate with the minmax algorithm before further + downsampling with LTTB.""") def _process(self, element, key=None): if isinstance(element, (Overlay, NdOverlay)): @@ -183,9 +245,12 @@ def _process(self, element, key=None): if ys.dtype == np.bool_: ys = ys.astype(np.int8) downsample = _ALGORITHMS[self.p.algorithm] + kwargs = {} if self.p.algorithm == "lttb" and isinstance(element, Area): raise NotImplementedError( "LTTB algorithm is not implemented for hv.Area" ) - samples = downsample(xs, ys, self.p.width) + elif self.p.algorithm == "minmax-lttb": + kwargs['minmax_ratio'] = self.p.minmax_ratio + samples = downsample(xs, ys, self.p.width, parallel=self.p.parallel, **kwargs) return element.iloc[samples] From 4c276ad75220fb0d484e50912eddcc428983c4e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20H=C3=B8xbro=20Hansen?= Date: Tue, 16 Jan 2024 09:48:33 +0100 Subject: [PATCH 2/4] Small refactoring to not raise exceptions --- holoviews/operation/downsample.py | 26 +++++++++++--------------- 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/holoviews/operation/downsample.py b/holoviews/operation/downsample.py index c85690c669..53e16681b8 100644 --- a/holoviews/operation/downsample.py +++ b/holoviews/operation/downsample.py @@ -102,7 +102,9 @@ def _lttb_inner(x, y, n_out, sampled_x, offset): def _lttb(x, y, n_out, **kwargs): """ - Downsample the data using the LTTB algorithm (python implementation). + Downsample the data using the LTTB algorithm. + + Will use a Python/Numpy implementation if tsdownsample is not available. Args: x (np.ndarray): The x-values of the data. @@ -114,10 +116,10 @@ def _lttb(x, y, n_out, **kwargs): try: from tsdownsample import LTTBDownsampler return LTTBDownsampler().downsample(x, y, n_out=n_out, **kwargs) - except Exception as e: - raise e except ModuleNotFoundError: pass + except Exception as e: + raise e # Bucket size. Leave room for start and end data points block_size = (y.shape[0] - 2) / (n_out - 2) @@ -158,37 +160,31 @@ def _nth_point(x, y, n_out, **kwargs): def _min_max(x, y, n_out, **kwargs): try: from tsdownsample import MinMaxDownsampler - except Exception as e: - raise e - except ModuleNotFoundError as e: + except ModuleNotFoundError: raise NotImplementedError( 'The min-max downsampling algorithm requires the tsdownsampler ' 'library to be installed.' - ) from e + ) from None return MinMaxDownsampler().downsample(x, y, n_out=n_out, **kwargs) def _min_max_lttb(x, y, n_out, **kwargs): try: from tsdownsample import MinMaxLTTBDownsampler - except Exception as e: - raise e - except ModuleNotFoundError as e: + except ModuleNotFoundError: raise NotImplementedError( 'The minmax-lttb downsampling algorithm requires the tsdownsampler ' 'library to be installed.' - ) from e + ) from None return MinMaxLTTBDownsampler().downsample(x, y, n_out=n_out, **kwargs) def _m4(x, y, n_out, **kwargs): try: from tsdownsample import M4Downsampler - except Exception as e: - raise e - except ModuleNotFoundError as e: + except ModuleNotFoundError: raise NotImplementedError( 'The m4 downsampling algorithm requires the tsdownsampler ' 'library to be installed.' - ) from e + ) from None return M4Downsampler().downsample(x, y, n_out=n_out, **kwargs) From 403f16d6aa4fa5ea3a08c403e86264d4cef4585b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20H=C3=B8xbro=20Hansen?= Date: Tue, 16 Jan 2024 10:11:58 +0100 Subject: [PATCH 3/4] lint --- holoviews/operation/downsample.py | 1 - 1 file changed, 1 deletion(-) diff --git a/holoviews/operation/downsample.py b/holoviews/operation/downsample.py index 53e16681b8..55c761bc51 100644 --- a/holoviews/operation/downsample.py +++ b/holoviews/operation/downsample.py @@ -194,7 +194,6 @@ def _m4(x, y, n_out, **kwargs): 'minmax': _min_max, 'minmax-lttb': _min_max_lttb, 'm4': _m4, - } class downsample1d(ResampleOperation1D): From 0e2316ce84d03f52408477e4f198c6746c548a36 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20H=C3=B8xbro=20Hansen?= Date: Wed, 17 Jan 2024 08:56:56 +0100 Subject: [PATCH 4/4] Update holoviews/operation/downsample.py --- holoviews/operation/downsample.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/holoviews/operation/downsample.py b/holoviews/operation/downsample.py index 55c761bc51..f75379e9e2 100644 --- a/holoviews/operation/downsample.py +++ b/holoviews/operation/downsample.py @@ -241,7 +241,7 @@ def _process(self, element, key=None): ys = ys.astype(np.int8) downsample = _ALGORITHMS[self.p.algorithm] kwargs = {} - if self.p.algorithm == "lttb" and isinstance(element, Area): + if "lttb" in self.p.algorithm and isinstance(element, Area): raise NotImplementedError( "LTTB algorithm is not implemented for hv.Area" )