From d13b20a99ecc5463db46a655ba7bf1d07cb300fa Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Fri, 11 Sep 2020 17:14:58 -0400 Subject: [PATCH 1/2] MNT: do not cache a version of inverse transform that can go stale --- lib/matplotlib/colors.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/matplotlib/colors.py b/lib/matplotlib/colors.py index 865b621d16d1..c6afe628cf19 100644 --- a/lib/matplotlib/colors.py +++ b/lib/matplotlib/colors.py @@ -1251,7 +1251,6 @@ def __init__(self, *args, **kwargs): **{k: ba.arguments.pop(k) for k in ["vmin", "vmax", "clip"]}) self._scale = scale_cls(axis=None, **ba.arguments) self._trf = self._scale.get_transform() - self._inv_trf = self._trf.inverted() def __call__(self, value, clip=None): value, is_scalar = self.process_value(value) @@ -1283,7 +1282,10 @@ def inverse(self, value): raise ValueError("Invalid vmin or vmax") rescaled = value * (t_vmax - t_vmin) rescaled += t_vmin - return self._inv_trf.transform(rescaled).reshape(np.shape(value)) + return (self._trf + .inverted() + .transform(rescaled) + .reshape(np.shape(value))) Norm.__name__ = base_norm_cls.__name__ Norm.__qualname__ = base_norm_cls.__qualname__ From b23708a5af1a9901e9b413f327a3ef20dfc6d45e Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Fri, 11 Sep 2020 17:15:30 -0400 Subject: [PATCH 2/2] FIX: special-case LogNorm when resealing closes #18415 We need to special case when rescaling the user input for interpolation with LogNorm. The issue is that due to the inherent precision limits of floating point math we can end up with errors on the order if 1e-18 of the overall data range when rescaling. If the bottom vlim is close to 0 and the top is order 10e20 than this error can result in the rescaled vmin being negative which then fails when we (temporarily) change the vmin/vmax on the norm. We started adjusting the vmin/vmax to work around the same issue with float precision in #17636 / 3dea5c7 to make sure that values exactly equal to the limits did not move across the boundary and get erroneously mapped is over/under. Long term we may want to add the ability for the norms to suggest to their clients what the constraints on the vmin/vmax are, but hard-coding a special case for LogNorm is the pragmatic solution. --- lib/matplotlib/image.py | 8 ++++++-- lib/matplotlib/tests/test_image.py | 19 +++++++++++++++++++ 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/lib/matplotlib/image.py b/lib/matplotlib/image.py index 2ef2226dc0bf..17b4da2d16f0 100644 --- a/lib/matplotlib/image.py +++ b/lib/matplotlib/image.py @@ -534,9 +534,13 @@ def _make_image(self, A, in_bbox, out_bbox, clip_bbox, magnification=1.0, resampled_masked = np.ma.masked_array(A_resampled, out_mask) # we have re-set the vmin/vmax to account for small errors # that may have moved input values in/out of range + s_vmin, s_vmax = vrange + if isinstance(self.norm, mcolors.LogNorm): + if s_vmin < 0: + s_vmin = max(s_vmin, np.finfo(scaled_dtype).eps) with cbook._setattr_cm(self.norm, - vmin=vrange[0], - vmax=vrange[1], + vmin=s_vmin, + vmax=s_vmax, ): output = self.norm(resampled_masked) else: diff --git a/lib/matplotlib/tests/test_image.py b/lib/matplotlib/tests/test_image.py index 1b9dd306a4dc..80803d576464 100644 --- a/lib/matplotlib/tests/test_image.py +++ b/lib/matplotlib/tests/test_image.py @@ -1204,3 +1204,22 @@ def test_imshow_quantitynd(): ax.imshow(arr) # executing the draw should not raise an exception fig.canvas.draw() + + +@check_figures_equal(extensions=['png']) +def test_huge_range_log(fig_test, fig_ref): + data = np.full((5, 5), -1, dtype=np.float64) + data[0:2, :] = 1E20 + + ax = fig_test.subplots() + im = ax.imshow(data, norm=colors.LogNorm(vmin=100, vmax=data.max()), + interpolation='nearest', cmap='viridis') + + data = np.full((5, 5), -1, dtype=np.float64) + data[0:2, :] = 1000 + + cm = copy(plt.get_cmap('viridis')) + cm.set_under('w') + ax = fig_ref.subplots() + im = ax.imshow(data, norm=colors.Normalize(vmin=100, vmax=data.max()), + interpolation='nearest', cmap=cm)