From d3ca16c154d057e88d14d2452a6627aa0fbdc278 Mon Sep 17 00:00:00 2001 From: Eric Firing Date: Tue, 21 May 2019 09:54:50 -1000 Subject: [PATCH 1/4] Fix bug in SymmetricalLogTransform. By failing the copy the input array, SymmetricalLogTransform.transform_non_affine was returning an array with points in the linear range masked. Closes #14265. Replaces #14281. --- lib/matplotlib/scale.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/matplotlib/scale.py b/lib/matplotlib/scale.py index 449368d8206c..a7ec0ff83623 100644 --- a/lib/matplotlib/scale.py +++ b/lib/matplotlib/scale.py @@ -489,7 +489,7 @@ def transform_non_affine(self, a): masked = ma.masked_inside(a, -self.linthresh, self.linthresh, - copy=False) + copy=True) log = sign * self.linthresh * ( self._linscale_adj + ma.log(np.abs(masked) / self.linthresh) / self._log_base) @@ -521,7 +521,7 @@ def __init__(self, base, linthresh, linscale): def transform_non_affine(self, a): sign = np.sign(a) masked = ma.masked_inside(a, -self.invlinthresh, - self.invlinthresh, copy=False) + self.invlinthresh, copy=True) exp = sign * self.linthresh * ( ma.power(self.base, (sign * (masked / self.linthresh)) - self._linscale_adj)) From b218e7c6d97283ccbc6185dbe287302fc280191a Mon Sep 17 00:00:00 2001 From: Eric Firing Date: Tue, 21 May 2019 15:01:45 -1000 Subject: [PATCH 2/4] SymmetricalLogTransform and inverse return same type as input. Their transform_non_affine methods have been updated following the strategy used in LogTransform so that they work correctly with masked arrays or plain ndarrays, but do not require any explicit np.ma functions. --- lib/matplotlib/scale.py | 37 +++++++++++++++---------------------- 1 file changed, 15 insertions(+), 22 deletions(-) diff --git a/lib/matplotlib/scale.py b/lib/matplotlib/scale.py index a7ec0ff83623..2ab7d04e4f71 100644 --- a/lib/matplotlib/scale.py +++ b/lib/matplotlib/scale.py @@ -485,18 +485,14 @@ def __init__(self, base, linthresh, linscale): self._log_base = np.log(base) def transform_non_affine(self, a): - sign = np.sign(a) - masked = ma.masked_inside(a, - -self.linthresh, - self.linthresh, - copy=True) - log = sign * self.linthresh * ( - self._linscale_adj + - ma.log(np.abs(masked) / self.linthresh) / self._log_base) - if masked.mask.any(): - return ma.where(masked.mask, a * self._linscale_adj, log) - else: - return log + abs_a = np.abs(a) + with np.errstate(divide="ignore", invalid="ignore"): + out = np.sign(a) * self.linthresh * ( + self._linscale_adj + + np.log(abs_a / self.linthresh) / self._log_base) + inside = abs_a <= self.linthresh + out[inside] = a[inside] * self._linscale_adj + return out def inverted(self): return InvertedSymmetricalLogTransform(self.base, self.linthresh, @@ -519,16 +515,13 @@ def __init__(self, base, linthresh, linscale): self._linscale_adj = (linscale / (1.0 - self.base ** -1)) def transform_non_affine(self, a): - sign = np.sign(a) - masked = ma.masked_inside(a, -self.invlinthresh, - self.invlinthresh, copy=True) - exp = sign * self.linthresh * ( - ma.power(self.base, (sign * (masked / self.linthresh)) - - self._linscale_adj)) - if masked.mask.any(): - return ma.where(masked.mask, a / self._linscale_adj, exp) - else: - return exp + abs_a = np.abs(a) + with np.errstate(divide="ignore", invalid="ignore"): + out = np.sign(a) * self.linthresh * ( + np.power(self.base, abs_a / self.linthresh - self._linscale_adj)) + inside = abs_a <= self.invlinthresh + out[inside] = a[inside] / self._linscale_adj + return out def inverted(self): return SymmetricalLogTransform(self.base, From e2c460e92677f779e3e17a35d02cd8259db02fed Mon Sep 17 00:00:00 2001 From: Eric Firing Date: Tue, 21 May 2019 16:57:58 -1000 Subject: [PATCH 3/4] Add a test for the transform and its inverse --- lib/matplotlib/tests/test_scale.py | 31 +++++++++++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/lib/matplotlib/tests/test_scale.py b/lib/matplotlib/tests/test_scale.py index c0ca6230ed33..3eee976a3e7f 100644 --- a/lib/matplotlib/tests/test_scale.py +++ b/lib/matplotlib/tests/test_scale.py @@ -1,9 +1,11 @@ from matplotlib.cbook import MatplotlibDeprecationWarning import matplotlib.pyplot as plt -from matplotlib.scale import Log10Transform, InvertedLog10Transform +from matplotlib.scale import (Log10Transform, InvertedLog10Transform, + SymmetricalLogTransform) from matplotlib.testing.decorators import check_figures_equal, image_comparison import numpy as np +from numpy.testing import assert_allclose import io import platform import pytest @@ -22,6 +24,33 @@ def test_log_scales(fig_test, fig_ref): ax_ref.plot(xlim, [24.1, 24.1], 'b') +def test_symlog_mask_nan(): + # Use a transform round-trip to verify that the forward and inverse + # transforms work, and that they respect nans and/or masking. + slt = SymmetricalLogTransform(10, 2, 1) + slti = slt.inverted() + + x = np.arange(-1.5, 5, 0.5) + out = slti.transform_non_affine(slt.transform_non_affine(x)) + assert_allclose(out, x) + assert type(out) == type(x) + + x[4] = np.nan + out = slti.transform_non_affine(slt.transform_non_affine(x)) + assert_allclose(out, x) + assert type(out) == type(x) + + x = np.ma.array(x) + out = slti.transform_non_affine(slt.transform_non_affine(x)) + assert_allclose(out, x) + assert type(out) == type(x) + + x[3] = np.ma.masked + out = slti.transform_non_affine(slt.transform_non_affine(x)) + assert_allclose(out, x) + assert type(out) == type(x) + + @image_comparison(['logit_scales.png'], remove_text=True) def test_logit_scales(): fig, ax = plt.subplots() From a904fab940c0b314f3f2e1b219d7051b52574d56 Mon Sep 17 00:00:00 2001 From: Eric Firing Date: Tue, 21 May 2019 17:39:32 -1000 Subject: [PATCH 4/4] Shorten a line. --- lib/matplotlib/scale.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/matplotlib/scale.py b/lib/matplotlib/scale.py index 2ab7d04e4f71..f0182347f7e8 100644 --- a/lib/matplotlib/scale.py +++ b/lib/matplotlib/scale.py @@ -518,7 +518,8 @@ def transform_non_affine(self, a): abs_a = np.abs(a) with np.errstate(divide="ignore", invalid="ignore"): out = np.sign(a) * self.linthresh * ( - np.power(self.base, abs_a / self.linthresh - self._linscale_adj)) + np.power(self.base, + abs_a / self.linthresh - self._linscale_adj)) inside = abs_a <= self.invlinthresh out[inside] = a[inside] / self._linscale_adj return out