Skip to content

New colormap normalizations: sqrt, arcsinh #1780

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
148 changes: 140 additions & 8 deletions lib/matplotlib/colors.py
Original file line number Diff line number Diff line change
Expand Up @@ -1000,7 +1000,140 @@ def autoscale_None(self, A):
self.vmin = ma.min(A)
if self.vmax is None:
self.vmax = ma.max(A)



class SqrtNorm(Normalize):
"""
Normalize a given value to the 0-1 range on a square (or n'th) root scale
"""
def __init__(self, vmin=None, vmax=None, clip=False, nthroot=2):
"""
nthroot allows cube roots, fourth roots, etc.
"""
self.vmin = vmin
self.vmax = vmax
self.clip = clip
self.nthroot = nthroot

def __call__(self, value, clip=None, midpoint=None):

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PEP8 compliance: there should only be one blank line here.

if clip is None:
clip = self.clip

if cbook.iterable(value):
vtype = 'array'
val = ma.asarray(value).astype(np.float)
else:
vtype = 'scalar'
val = ma.array([value]).astype(np.float)

self.autoscale_None(val)
vmin, vmax = self.vmin, self.vmax

if vmin > vmax:
raise ValueError("minvalue must be less than or equal to maxvalue")
elif vmin == vmax:
return 0.0 * val
else:
if clip:
mask = ma.getmask(val)
val = ma.array(np.clip(val.filled(vmax), vmin, vmax),
mask=mask)
result = (val-vmin) * (1.0/(vmax-vmin))
#(arcsinh(val)-arcsinh(vmin))/(arcsinh(vmax)-arcsinh(vmin))
result = result**(1./self.nthroot)
if vtype == 'scalar':
result = result[0]
return result

def autoscale_None(self, A):
""" autoscale only None-valued vmin or vmax """
if self.vmin is None:
self.vmin = ma.min(A)
if self.vmax is None:
self.vmax = ma.max(A)

def inverse(self, value):
if not self.scaled():
raise ValueError("Not invertible until scaled")
vmin, vmax = self.vmin, self.vmax

if cbook.iterable(value):
val = ma.asarray(value)
# r = sqrt(v-vmin/(vmax-vmin))
# v = r**2 * (vmax-vmin) + vmin
return val**self.nthroot * (vmax-vmin) + vmin
else:
return value**self.nthroot * (vmax-vmin) + vmin


class AsinhNorm(Normalize):
"""
Normalize a range of values to 0-1 on an arcsinh scale (nice alternative to
SymLogNormalize)
"""
def __init__(self, vmin=None, vmax=None, clip=False, vmid=None):
self.vmid = vmid
self.vmin = vmin
self.vmax = vmax
self.clip = clip

def __call__(self, value, clip=None, midpoint=None):

if clip is None:
clip = self.clip

if cbook.iterable(value):
vtype = 'array'
val = ma.asarray(value).astype(np.float)
else:
vtype = 'scalar'
val = ma.array([value]).astype(np.float)

self.autoscale_None(val)
vmin, vmax = self.vmin, self.vmax

vmid = self.vmid if self.vmid is not None else (vmax+vmin)/2.0

if midpoint is None:
midpoint = (vmid - vmin) / (vmax - vmin)

if vmin > vmax:
raise ValueError("minvalue must be less than or equal to maxvalue")
elif vmin == vmax:
return 0.0 * val
else:
if clip:
mask = ma.getmask(val)
val = ma.array(np.clip(val.filled(vmax), vmin, vmax),
mask=mask)
result = (val-vmin) * (1.0/(vmax-vmin))
result = ma.arcsinh(result/midpoint) / ma.arcsinh(1./midpoint)
if vtype == 'scalar':
result = result[0]
return result

def autoscale_None(self, A):
' autoscale only None-valued vmin or vmax'
if self.vmin is None:
self.vmin = ma.min(A)
if self.vmax is None:
self.vmax = ma.max(A)
if self.vmid is None:
self.vmid = (self.vmax+self.vmin)/2.0

def inverse(self, value):
if not self.scaled():
raise ValueError("Not invertible until scaled")
vmin, vmax = self.vmin, self.vmax

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PEP compliance: there should be only 2 blank lines here.

if cbook.iterable(value):
val = ma.asarray(value)
# r = arcsinh(v-vmin/(vmax-vmin) / midpoint) / arcsinh(1/midpoint)
# v = sinh(r * arcsinh(1/midpoint)) * midpoint * (vmax-vmin) + vmin
return np.sinh(val * np.arcsinh(1./midpoint)) * midpoint * (vmax-vmin) + vmin
else:
return np.sinh(value * np.arcsinh(1./midpoint)) * midpoint * (vmax-vmin) + vmin

class SymLogNorm(Normalize):
"""
Expand Down Expand Up @@ -1039,7 +1172,7 @@ def __call__(self, value, clip=None):
result, is_scalar = self.process_value(value)
self.autoscale_None(result)
vmin, vmax = self.vmin, self.vmax

if vmin > vmax:
raise ValueError("minvalue must be less than or equal to maxvalue")
elif vmin == vmax:
Expand All @@ -1048,7 +1181,7 @@ def __call__(self, value, clip=None):
if clip:
mask = ma.getmask(result)
result = ma.array(np.clip(result.filled(vmax), vmin, vmax),
mask=mask)
mask=mask)
# in-place equivalent of above can be much faster
resdat = self._transform(result.data)
resdat -= self._lower
Expand All @@ -1057,8 +1190,8 @@ def __call__(self, value, clip=None):
if is_scalar:
result = result[0]
return result
def _transform(self, a):

def _transform(self, a):
"""
Inplace transformation.
"""
Expand All @@ -1069,7 +1202,7 @@ def _transform(self, a):
a[masked] = log
a[~masked] *= self._linscale_adj
return a

def _inv_transform(self, a):
"""
Inverse inplace Transformation.
Expand All @@ -1081,7 +1214,7 @@ def _inv_transform(self, a):
a[masked] = exp
a[~masked] /= self._linscale_adj
return a

def _transform_vmin_vmax(self):
"""
Calculates vmin and vmax in the transformed system.
Expand All @@ -1090,7 +1223,6 @@ def _transform_vmin_vmax(self):
arr = np.array([vmax, vmin])
self._upper, self._lower = self._transform(arr)


def inverse(self, value):
if not self.scaled():
raise ValueError("Not invertible until scaled")
Expand Down
44 changes: 39 additions & 5 deletions lib/matplotlib/tests/test_colors.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import matplotlib.colors as mcolors
import matplotlib.cm as cm


def test_colormap_endian():
"""
Github issue #1005: a bug in putmask caused erroneous
Expand All @@ -23,6 +24,7 @@ def test_colormap_endian():
#print(anative.dtype.isnative, aforeign.dtype.isnative)
assert_array_equal(cmap(anative), cmap(aforeign))


def test_BoundaryNorm():
"""
Github issue #1258: interpolation was failing with numpy
Expand All @@ -36,16 +38,17 @@ def test_BoundaryNorm():
ncolors = len(boundaries)
bn = mcolors.BoundaryNorm(boundaries, ncolors)
assert_array_equal(bn(vals), expected)



def test_LogNorm():
"""
LogNorm igornoed clip, now it has the same
behavior as Normalize, e.g. values > vmax are bigger than 1
without clip, with clip they are 1.
LogNorm ignored clip, now it has the same behavior as Normalize, e.g.
values > vmax are bigger than 1 without clip, with clip they are 1.
"""
ln = mcolors.LogNorm(clip=True, vmax=5)
assert_array_equal(ln([1, 6]), [0, 1.0])


def test_Normalize():
norm = mcolors.Normalize()
vals = np.arange(-10, 10, 1, dtype=np.float)
Expand All @@ -61,19 +64,49 @@ def test_SymLogNorm():
norm = mcolors.SymLogNorm(3, vmax=5, linscale=1.2)
vals = np.array([-30, -1, 2, 6], dtype=np.float)
normed_vals = norm(vals)
expected = [ 0., 0.53980074, 0.826991, 1.02758204]
expected = [0., 0.53980074, 0.826991, 1.02758204]
assert_array_almost_equal(normed_vals, expected)
_inverse_tester(norm, vals)
_scalar_tester(norm, vals)
_mask_tester(norm, vals)


def test_SqrtNorm():
"""
Test SqrtNorm behavior
"""
norm = mcolors.SqrtNorm(vmin=-3, vmax=5)
vals = np.array([-30, -1, 2, 6], dtype=np.float)
normed_vals = norm(vals)
expected = [np.nan, 0.5, 0.79056942, 1.06066017]
assert_array_almost_equal(normed_vals, expected)
_inverse_tester(norm, vals) # note that this accepts NaN == -30 because it is masked
_scalar_tester(norm, vals)
_mask_tester(norm, vals)


def test_AsinhNorm():
"""
Test AsinhNorm behavior
"""
norm = mcolors.AsinhNorm(vmin=-3, vmax=5, vmid=0)
vals = np.array([-30, -1, 2, 6], dtype=np.float)
normed_vals = norm(vals)
expected = [-1.694637815353593, 0.36613618958718575, 0.751895902558991,
1.0650312050423278]
assert_array_almost_equal(normed_vals, expected)
_inverse_tester(norm, vals) # note that this accepts NaN == -30 because it is masked
_scalar_tester(norm, vals)
_mask_tester(norm, vals)


def _inverse_tester(norm_instance, vals):
"""
Checks if the inverse of the given normalization is working.
"""
assert_array_almost_equal(norm_instance.inverse(norm_instance(vals)), vals)


def _scalar_tester(norm_instance, vals):
"""
Checks if scalars and arrays are handled the same way.
Expand All @@ -82,6 +115,7 @@ def _scalar_tester(norm_instance, vals):
scalar_result = [norm_instance(float(v)) for v in vals]
assert_array_almost_equal(scalar_result, norm_instance(vals))


def _mask_tester(norm_instance, vals):
"""
Checks mask handling
Expand Down