From 73ae1d3af718ac932f00ab5576132741dc3d51d9 Mon Sep 17 00:00:00 2001 From: David Stansby Date: Mon, 20 Jan 2020 20:19:36 +0000 Subject: [PATCH 1/8] Re-write sym-log-norm --- lib/matplotlib/colors.py | 66 ++++++++++++++--------------- lib/matplotlib/tests/test_colors.py | 12 ++++-- 2 files changed, 40 insertions(+), 38 deletions(-) diff --git a/lib/matplotlib/colors.py b/lib/matplotlib/colors.py index 9f58a03a9f90..ed956b282dd6 100644 --- a/lib/matplotlib/colors.py +++ b/lib/matplotlib/colors.py @@ -1231,9 +1231,10 @@ def __init__(self, linthresh, linscale=1.0, """ Normalize.__init__(self, vmin, vmax, clip) self.linthresh = float(linthresh) - self._linscale_adj = (linscale / (1.0 - np.e ** -1)) - if vmin is not None and vmax is not None: - self._transform_vmin_vmax() + # Number of decades in the log region + ndec = np.log10(vmax / linthresh) + # Size of the linear region from 0 to linthresh in the transformed space + self.linear_size = 1 / (1 + linscale / ndec) def __call__(self, value, clip=None): if clip is None: @@ -1254,57 +1255,52 @@ def __call__(self, value, clip=None): mask=mask) # in-place equivalent of above can be much faster resdat = self._transform(result.data) - resdat -= self._lower - resdat /= (self._upper - self._lower) if is_scalar: result = result[0] return result def _transform(self, a): - """Inplace transformation.""" + """In-place mapping from *a* to [0, 1]""" with np.errstate(invalid="ignore"): - masked = np.abs(a) > self.linthresh - sign = np.sign(a[masked]) - log = (self._linscale_adj + np.log(np.abs(a[masked]) / self.linthresh)) - log *= sign * self.linthresh - a[masked] = log - a[~masked] *= self._linscale_adj + logregion = np.abs(a) > self.linthresh + + # Transform log value + sign = np.sign(a[logregion]) + log = (1 - self.linear_size) * np.log10(np.abs(a[logregion])) + self.linear_size + a[logregion] = log * sign + + # Transform linear values + a[~logregion] *= self.linear_size / self.linthresh + + # Transform from [-1, 1] to [0, 1] + a += 1 + a /= 2 return a def _inv_transform(self, a): """Inverse inplace Transformation.""" - masked = np.abs(a) > (self.linthresh * self._linscale_adj) - sign = np.sign(a[masked]) - exp = np.exp(sign * a[masked] / self.linthresh - self._linscale_adj) - exp *= sign * self.linthresh - a[masked] = exp - a[~masked] /= self._linscale_adj + # Transform from [0, 1] to [-1, 1] + a *= 2 + a -= 1 + + # Transform back log values + logregion = np.abs(a) > self.linear_size + sign = np.sign(a[logregion]) + exp = 10**((np.abs(a[logregion]) - self.linear_size) / + (1 - self.linear_size)) + a[logregion] = exp * sign + + # Transform back linear values + a[~logregion] /= self.linear_size / self.linthresh return a - def _transform_vmin_vmax(self): - """Calculates vmin and vmax in the transformed system.""" - vmin, vmax = self.vmin, self.vmax - arr = np.array([vmax, vmin]).astype(float) - self._upper, self._lower = self._transform(arr) - def inverse(self, value): if not self.scaled(): raise ValueError("Not invertible until scaled") val = np.ma.asarray(value) - val = val * (self._upper - self._lower) + self._lower return self._inv_transform(val) - def autoscale(self, A): - # docstring inherited. - super().autoscale(A) - self._transform_vmin_vmax() - - def autoscale_None(self, A): - # docstring inherited. - super().autoscale_None(A) - self._transform_vmin_vmax() - class PowerNorm(Normalize): """ diff --git a/lib/matplotlib/tests/test_colors.py b/lib/matplotlib/tests/test_colors.py index 05a81e26def3..d7d854b8abfc 100644 --- a/lib/matplotlib/tests/test_colors.py +++ b/lib/matplotlib/tests/test_colors.py @@ -395,9 +395,7 @@ def test_TwoSlopeNorm_premature_scaling(): def test_SymLogNorm(): - """ - Test SymLogNorm behavior - """ + # Test SymLogNorm behavior norm = mcolors.SymLogNorm(3, vmax=5, linscale=1.2) vals = np.array([-30, -1, 2, 6], dtype=float) normed_vals = norm(vals) @@ -413,6 +411,14 @@ def test_SymLogNorm(): assert_array_almost_equal(normed_vals, expected) +@pytest.mark.parametrize('val,normed', + ([10, 1], [1, 0.75], [0, 0.5], [-1, 0.25], [-10, 0])) +def test_symlognorm_vals(val, normed): + norm = mcolors.SymLogNorm(linthresh=1, vmin=-10, vmax=10, linscale=1) + assert_array_almost_equal(norm(val), normed) + assert_array_almost_equal(norm.inverse(norm(val)), val) + + def test_SymLogNorm_colorbar(): """ Test un-called SymLogNorm in a colorbar. From 943a8485c42d58e954456c852d8687b9297beeae Mon Sep 17 00:00:00 2001 From: David Stansby Date: Mon, 3 Feb 2020 10:18:37 +0000 Subject: [PATCH 2/8] Allow specifying base --- lib/matplotlib/colors.py | 30 ++++++++++++++++++++---------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/lib/matplotlib/colors.py b/lib/matplotlib/colors.py index ed956b282dd6..f156e73d2d35 100644 --- a/lib/matplotlib/colors.py +++ b/lib/matplotlib/colors.py @@ -1214,7 +1214,7 @@ class SymLogNorm(Normalize): (-*linthresh*, *linthresh*). """ def __init__(self, linthresh, linscale=1.0, - vmin=None, vmax=None, clip=False): + vmin=None, vmax=None, clip=False, base=10): """ Parameters ---------- @@ -1228,13 +1228,23 @@ def __init__(self, linthresh, linscale=1.0, example, when *linscale* == 1.0 (the default), the space used for the positive and negative halves of the linear range will be equal to one decade in the logarithmic range. + base : float + The base... """ Normalize.__init__(self, vmin, vmax, clip) self.linthresh = float(linthresh) - # Number of decades in the log region - ndec = np.log10(vmax / linthresh) + self.base = float(base) + self.linscale = float(linscale) + + @property + def _linear_size(self): # Size of the linear region from 0 to linthresh in the transformed space - self.linear_size = 1 / (1 + linscale / ndec) + ndec = self._logbase(self.vmax / self.linthresh) + return 1 / (1 + self.linscale / ndec) + + def _logbase(self, val): + 'Take the log of val in the base `self.base`' + return np.log(val) / np.log(self.base) def __call__(self, value, clip=None): if clip is None: @@ -1267,11 +1277,11 @@ def _transform(self, a): # Transform log value sign = np.sign(a[logregion]) - log = (1 - self.linear_size) * np.log10(np.abs(a[logregion])) + self.linear_size + log = (1 - self._linear_size) * self._logbase(np.abs(a[logregion])) + self._linear_size a[logregion] = log * sign # Transform linear values - a[~logregion] *= self.linear_size / self.linthresh + a[~logregion] *= self._linear_size / self.linthresh # Transform from [-1, 1] to [0, 1] a += 1 @@ -1285,14 +1295,14 @@ def _inv_transform(self, a): a -= 1 # Transform back log values - logregion = np.abs(a) > self.linear_size + logregion = np.abs(a) > self._linear_size sign = np.sign(a[logregion]) - exp = 10**((np.abs(a[logregion]) - self.linear_size) / - (1 - self.linear_size)) + exp = self.base**((np.abs(a[logregion]) - self._linear_size) / + (1 - self._linear_size)) a[logregion] = exp * sign # Transform back linear values - a[~logregion] /= self.linear_size / self.linthresh + a[~logregion] /= self._linear_size / self.linthresh return a def inverse(self, value): From 60f0d69427655921e751c8191abeeccde0c0db6c Mon Sep 17 00:00:00 2001 From: David Stansby Date: Mon, 3 Feb 2020 13:33:45 +0000 Subject: [PATCH 3/8] Disallow vmin!=vmax --- lib/matplotlib/colors.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/matplotlib/colors.py b/lib/matplotlib/colors.py index f156e73d2d35..de3d4b205c5f 100644 --- a/lib/matplotlib/colors.py +++ b/lib/matplotlib/colors.py @@ -1231,6 +1231,9 @@ def __init__(self, linthresh, linscale=1.0, base : float The base... """ + if vmin is not None and vmax is not None and vmin != -vmax: + raise ValueError('SymLogNorm only works for vmin = -vmax ' + f'(got vmin={vmin}, vmax={vmax})') Normalize.__init__(self, vmin, vmax, clip) self.linthresh = float(linthresh) self.base = float(base) @@ -1238,7 +1241,8 @@ def __init__(self, linthresh, linscale=1.0, @property def _linear_size(self): - # Size of the linear region from 0 to linthresh in the transformed space + 'Return the size of the linear portion in transformed space' + # Number of decades in the logarithmic range ndec = self._logbase(self.vmax / self.linthresh) return 1 / (1 + self.linscale / ndec) From 6fdd2e066dcf04de405773d44a94798ac09cfa1c Mon Sep 17 00:00:00 2001 From: David Stansby Date: Mon, 3 Feb 2020 13:33:53 +0000 Subject: [PATCH 4/8] Update tests --- lib/matplotlib/tests/test_colors.py | 34 +++++++++++++++++++---------- 1 file changed, 23 insertions(+), 11 deletions(-) diff --git a/lib/matplotlib/tests/test_colors.py b/lib/matplotlib/tests/test_colors.py index d7d854b8abfc..a3a87274685e 100644 --- a/lib/matplotlib/tests/test_colors.py +++ b/lib/matplotlib/tests/test_colors.py @@ -396,27 +396,39 @@ def test_TwoSlopeNorm_premature_scaling(): def test_SymLogNorm(): # Test SymLogNorm behavior - norm = mcolors.SymLogNorm(3, vmax=5, linscale=1.2) + norm = mcolors.SymLogNorm(linthresh=3, vmax=5, linscale=1.2, base=np.e) vals = np.array([-30, -1, 2, 6], dtype=float) normed_vals = norm(vals) - expected = [0., 0.53980074, 0.826991, 1.02758204] + expected = [-0.842119, 0.450236, 0.599528, 1.277676] assert_array_almost_equal(normed_vals, expected) _inverse_tester(norm, vals) _scalar_tester(norm, vals) _mask_tester(norm, vals) - # Ensure that specifying vmin returns the same result as above - norm = mcolors.SymLogNorm(3, vmin=-30, vmax=5, linscale=1.2) - normed_vals = norm(vals) - assert_array_almost_equal(normed_vals, expected) +def test_symlognorm_vals(): + vals = [-10, -1, 0, 1, 10] -@pytest.mark.parametrize('val,normed', - ([10, 1], [1, 0.75], [0, 0.5], [-1, 0.25], [-10, 0])) -def test_symlognorm_vals(val, normed): norm = mcolors.SymLogNorm(linthresh=1, vmin=-10, vmax=10, linscale=1) - assert_array_almost_equal(norm(val), normed) - assert_array_almost_equal(norm.inverse(norm(val)), val) + normed_vals = norm(vals) + expected = [0, 0.25, 0.5, 0.75, 1] + assert_array_almost_equal(norm(vals), normed_vals) + assert_array_almost_equal(norm.inverse(norm(vals)), vals) + + # If we increase linscale to 2, the space for the linear range [0, 1] + # should be twice as large as the space for the logarithmic range [1, 10] + norm = mcolors.SymLogNorm(linthresh=1, vmin=-10, vmax=10, linscale=2) + normed_vals = norm(vals) + expected = [0, 1/6, 0.5, 5/6, 1] + assert_array_almost_equal(norm(vals), normed_vals) + assert_array_almost_equal(norm.inverse(norm(vals)), vals) + + # Similarly, going the other way means the linear range should shrink + norm = mcolors.SymLogNorm(linthresh=1, vmin=-10, vmax=10, linscale=0.5) + normed_vals = norm(vals) + expected = [0, 2/6, 0.5, 4/6, 1] + assert_array_almost_equal(norm(vals), normed_vals) + assert_array_almost_equal(norm.inverse(norm(vals)), vals) def test_SymLogNorm_colorbar(): From cc1fa93ecb7c002a98f1527b3d13fd4c73045964 Mon Sep 17 00:00:00 2001 From: David Stansby Date: Mon, 3 Feb 2020 13:38:55 +0000 Subject: [PATCH 5/8] Add base docs --- lib/matplotlib/colors.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/lib/matplotlib/colors.py b/lib/matplotlib/colors.py index de3d4b205c5f..d541604d3c4e 100644 --- a/lib/matplotlib/colors.py +++ b/lib/matplotlib/colors.py @@ -1215,7 +1215,7 @@ class SymLogNorm(Normalize): """ def __init__(self, linthresh, linscale=1.0, vmin=None, vmax=None, clip=False, base=10): - """ + r""" Parameters ---------- linthresh : float @@ -1228,8 +1228,15 @@ def __init__(self, linthresh, linscale=1.0, example, when *linscale* == 1.0 (the default), the space used for the positive and negative halves of the linear range will be equal to one decade in the logarithmic range. - base : float - The base... + base : float, default: 10 + The base in which the number of decades in the log range is + calculated. For example, if ``base=2``, ``linthresh=2`` and + ``vmax=6``, the number of decades is :math:`\log_{2} (6 - 2) = 4`. + + Notes + ----- + Currently SymLogNorm only works with ``vmin == -vmax``, such that an + input of 0 always maps to half-way in the scale. """ if vmin is not None and vmax is not None and vmin != -vmax: raise ValueError('SymLogNorm only works for vmin = -vmax ' @@ -1281,7 +1288,8 @@ def _transform(self, a): # Transform log value sign = np.sign(a[logregion]) - log = (1 - self._linear_size) * self._logbase(np.abs(a[logregion])) + self._linear_size + log = ((1 - self._linear_size) * self._logbase(np.abs(a[logregion])) + + self._linear_size) a[logregion] = log * sign # Transform linear values From 890465499101391fcebe063dbabb14f79c132b7b Mon Sep 17 00:00:00 2001 From: David Stansby Date: Mon, 3 Feb 2020 13:45:03 +0000 Subject: [PATCH 6/8] Add API change --- .../prev_api_changes/api_changes_3.2.0/behavior.rst | 12 +++++++++++- lib/matplotlib/colors.py | 2 +- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/doc/api/prev_api_changes/api_changes_3.2.0/behavior.rst b/doc/api/prev_api_changes/api_changes_3.2.0/behavior.rst index 0d8933fb363b..58528fe6dd4d 100644 --- a/doc/api/prev_api_changes/api_changes_3.2.0/behavior.rst +++ b/doc/api/prev_api_changes/api_changes_3.2.0/behavior.rst @@ -2,9 +2,19 @@ Behavior changes ---------------- +Fixed calculations in `.SymLogNorm` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +The symmetrical log normalizer was previously returning erroneous values, so has +be re-written to give the correct behaviour. Aside from returning different +values: + +- The base in which the number of decades in the log range is calculated can + now be specified by the ``base`` keyword argument, which defaults to 10. +- The behaviour when passing both ``vmin`` and ``vmax`` which were not negative + of each other is ill-defined, so this has been disallowed. + Reduced default value of :rc:`axes.formatter.limits` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - Changed the default value of :rc:`axes.formatter.limits` from -7, 7 to -5, 6 for better readability. diff --git a/lib/matplotlib/colors.py b/lib/matplotlib/colors.py index d541604d3c4e..cd58bd4f3537 100644 --- a/lib/matplotlib/colors.py +++ b/lib/matplotlib/colors.py @@ -1231,7 +1231,7 @@ def __init__(self, linthresh, linscale=1.0, base : float, default: 10 The base in which the number of decades in the log range is calculated. For example, if ``base=2``, ``linthresh=2`` and - ``vmax=6``, the number of decades is :math:`\log_{2} (6 - 2) = 4`. + ``vmax=6``, the number of decades is :math:`\log_{2} (6 - 2) = 2`. Notes ----- From 3fb1dcff1f5f008169c56a95cc7ec866aa55577a Mon Sep 17 00:00:00 2001 From: David Stansby Date: Mon, 3 Feb 2020 13:56:20 +0000 Subject: [PATCH 7/8] Check a different base --- lib/matplotlib/colors.py | 2 +- lib/matplotlib/tests/test_colors.py | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/lib/matplotlib/colors.py b/lib/matplotlib/colors.py index cd58bd4f3537..7906d21cda74 100644 --- a/lib/matplotlib/colors.py +++ b/lib/matplotlib/colors.py @@ -1231,7 +1231,7 @@ def __init__(self, linthresh, linscale=1.0, base : float, default: 10 The base in which the number of decades in the log range is calculated. For example, if ``base=2``, ``linthresh=2`` and - ``vmax=6``, the number of decades is :math:`\log_{2} (6 - 2) = 2`. + ``vmax=8``, the number of decades is :math:`\log_{2} (8 / 2) = 2`. Notes ----- diff --git a/lib/matplotlib/tests/test_colors.py b/lib/matplotlib/tests/test_colors.py index a3a87274685e..75bddd496ec6 100644 --- a/lib/matplotlib/tests/test_colors.py +++ b/lib/matplotlib/tests/test_colors.py @@ -430,6 +430,14 @@ def test_symlognorm_vals(): assert_array_almost_equal(norm(vals), normed_vals) assert_array_almost_equal(norm.inverse(norm(vals)), vals) + # Now check a different base to base 10 + vals = [-8, 4, -2, 0, 2, 4, 8] + norm = mcolors.SymLogNorm(linthresh=2, vmax=8, linscale=1, base=2) + normed_vals = norm(vals) + expected = [0, 1/8, 2/8, 3/8, 0.5, 5/8, 6/8, 7/8, 1] + assert_array_almost_equal(norm(vals), normed_vals) + assert_array_almost_equal(norm.inverse(norm(vals)), vals) + def test_SymLogNorm_colorbar(): """ From 82052d8b83d349367146886528e6077fadad9dbb Mon Sep 17 00:00:00 2001 From: David Stansby Date: Mon, 3 Feb 2020 15:00:09 +0000 Subject: [PATCH 8/8] Fix color test --- lib/matplotlib/tests/test_colors.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/matplotlib/tests/test_colors.py b/lib/matplotlib/tests/test_colors.py index 75bddd496ec6..062f1dc9ae38 100644 --- a/lib/matplotlib/tests/test_colors.py +++ b/lib/matplotlib/tests/test_colors.py @@ -933,7 +933,7 @@ def __add__(self, other): for norm in [mcolors.Normalize(), mcolors.LogNorm(), mcolors.SymLogNorm(3, vmax=5, linscale=1), mcolors.Normalize(vmin=mydata.min(), vmax=mydata.max()), - mcolors.SymLogNorm(3, vmin=mydata.min(), vmax=mydata.max()), + mcolors.SymLogNorm(3, vmin=-10, vmax=10), mcolors.PowerNorm(1)]: assert_array_equal(norm(mydata), norm(data)) fig, ax = plt.subplots() pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy