-
-
Notifications
You must be signed in to change notification settings - Fork 7.9k
ENH: Added FuncNorm #7631
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
ENH: Added FuncNorm #7631
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
- Loading branch information
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -965,30 +965,43 @@ class FuncNorm(Normalize): | |
Creates a normalizer using a custom function | ||
|
||
The normalizer will be a function mapping the data values into colormap | ||
values in the [0,1] range. | ||
values in the ``[0,1]`` range. | ||
""" | ||
|
||
def __init__(self, f, finv=None, **normalize_kw): | ||
def __init__(self, f, finv=None, vmin=None, vmax=None, clip=False): | ||
""" | ||
Specify the function to be used, and its inverse, as well as other | ||
parameters to be passed to `Normalize`. The normalization will be | ||
calculated as (f(x)-f(vmin))/(f(max)-f(vmin)). | ||
calculated as (f(x)-f(vmin))/(f(vmax)-f(vmin)). | ||
|
||
Parameters | ||
---------- | ||
f : callable or string | ||
Function to be used for the normalization receiving a single | ||
parameter, compatible with scalar values and ndarrays. | ||
Alternatively a string from the list ['linear', 'quadratic', | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Just point to the _StrFunctionParser (Alternatively any string supported by _StrFunctionParser) documentation 'cause otherwise this list will have to be updated every time that function is updated. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I was thinking that, but it's private; should we be linking to it here? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, this is precisely why I did not do it. I do not want to encourage people to have any direct contact _StringFuncParser. They should just see the result, so at least we can still switch to something else in the future if something better than that comes up. The best way to solve this would probably be to expose the available strings through a public helper function acting as an interface between _StringFuncParser and the values. Something like There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @QuLogic I get, but we sort of are linking to it since it's the underlying engine...dunno There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. An alternative would be to put the list of strings and the explanation of "p" in the Notes section. The advantage is that it would set it apart, and keep the Parameters block from being so long. The disadvantage is that it might be separating it too much from its parameter. It's up to you. |
||
'cubic', 'sqrt', 'cbrt','log', 'log10', 'power{a}', 'root{a}', | ||
'log(x+{a})', 'log10(x+{a})'] can be used, replacing 'a' by a | ||
number different than 0 when necessary. | ||
'cubic', 'x**{p}', 'sqrt', 'cbrt', 'root{p}(x)', 'log', 'log10', | ||
'log2', 'log{p}(x)', 'log(x+{p}) 'log10(x+{p})', 'log{p}(x+{p})] | ||
can be used, replacing 'p' by the corresponding value of the | ||
parameter, when present. | ||
finv : callable, optional | ||
Inverse function of `f` that satisfies finv(f(x))==x. It is | ||
optional in cases where `f` is provided as a string. | ||
normalize_kw : dict, optional | ||
Dict with keywords (`vmin`,`vmax`,`clip`) passed | ||
to `matplotlib.colors.Normalize`. | ||
Inverse function of `f` that satisfies finv(f(x))==x. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would prefer to see more concise docstrings, comments, and code, in general. I won't try to identify every opportunity for shortening things, but I will make some suggestions. Here, the line could be "Inverse of |
||
Optional/ignored when `f` is a string. | ||
vmin : float or None, optional | ||
Value assigned to the lower limit of the colormap. If None, it | ||
will be assigned to the minimum value of the data provided. | ||
Default None. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Here you could combine vmin with vmax, reverse the order (-> "vmin, vmax: None or float, optional") and delete the "Default None" line. Then, just "Data values to be mapped to 0 and 1. If either is None, it is assigned the minimum or maximum value of the data supplied to the first call of the norm." Let's leave the word "colormap" out, using it only where necessary, as in the clip explanation. |
||
vmax : float or None, optional | ||
Value assigned to the upper limit of the colormap. If None, it | ||
will be assigned to the maximum value of the data provided. | ||
Default None. | ||
clip : bool, optional | ||
If True, any value below `vmin` will be clipped to `vmin`, and | ||
any value above `vmax` will be clip to `vmin`. This effectively | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 'clip : bool, optional, default is False' and then delete the last line of the docstring. In addition to being more concise, having the default up front makes it more obvious. Then, 'If True, clip data values to [vmin, vmax]. This defeats ... colormap. If False, ... respectively.' There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As far as I know, the default option is specified on the description, not in the specification, right?
|
||
defeats the purpose of setting the over and under values of the | ||
color map. If False, values below `vmin` and above `vmax` will | ||
be set to -0.1 and 1.1 respectively, after the normalization. | ||
Default False. | ||
|
||
Examples | ||
-------- | ||
|
@@ -997,14 +1010,15 @@ def __init__(self, f, finv=None, **normalize_kw): | |
>>> import matplotlib.colors as colors | ||
>>> norm = colors.FuncNorm(f='log10', vmin=0.01, vmax=2) | ||
|
||
Or doing it manually: | ||
Or manually: | ||
|
||
>>> import matplotlib.colors as colors | ||
>>> norm = colors.FuncNorm(f=lambda x: np.log10(x), | ||
... finv=lambda x: 10.**(x), | ||
... vmin=0.01, vmax=2) | ||
|
||
""" | ||
super(FuncNorm, self).__init__(vmin=vmin, vmax=vmax, clip=clip) | ||
|
||
if isinstance(f, six.string_types): | ||
func_parser = cbook._StringFuncParser(f) | ||
|
@@ -1021,32 +1035,30 @@ def __init__(self, f, finv=None, **normalize_kw): | |
self._f = f | ||
self._finv = finv | ||
|
||
super(FuncNorm, self).__init__(**normalize_kw) | ||
|
||
def _update_f(self, vmin, vmax): | ||
# This method is to be used by derived classes in cases where | ||
# the limits vmin and vmax may require changing/updating the | ||
# function depending on vmin/vmax, for example rescaling it | ||
# to accomodate to the new interval. | ||
# to accommodate to the new interval. | ||
return | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This should be "pass", not "return". "pass" is the "do nothing" word. |
||
|
||
def __call__(self, value, clip=None): | ||
""" | ||
Normalizes `value` data in the `[vmin, vmax]` interval into | ||
the `[0.0, 1.0]` interval and returns it. | ||
Normalizes `value` data in the ``[vmin, vmax]`` interval into | ||
the ``[0.0, 1.0]`` interval and returns it. | ||
|
||
Parameters | ||
---------- | ||
value : float or ndarray of floats | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It can be a masked array, to handle missing values, or a python sequence, and it doesn't have to be float. So maybe just say "scalar or array-like". |
||
Data to be normalized. | ||
clip : boolean, optional | ||
Whether to clip the data outside the `[vmin, vmax]` limits. | ||
Whether to clip the data outside the ``[`vmin`, `vmax`]`` limits. | ||
Default `self.clip` from `Normalize` (which defaults to `False`). | ||
|
||
Returns | ||
------- | ||
result : masked array of floats | ||
Normalized data to the `[0.0, 1.0]` interval. If clip == False, | ||
Normalized data to the ``[0.0, 1.0]`` interval. If `clip` == False, | ||
values smaller than `vmin` or greater than `vmax` will be clipped | ||
to -0.1 and 1.1 respectively. | ||
|
||
|
@@ -1057,6 +1069,7 @@ def __call__(self, value, clip=None): | |
result, is_scalar = self.process_value(value) | ||
self.autoscale_None(result) | ||
|
||
self._check_vmin_vmax() | ||
vmin = float(self.vmin) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. does self.vmin/self.vmin need to be converted to float? I think there's an import at the top that forces division to always be floating point... There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is a very good point, I did not noticed that, I guess there is no need, in that case. Thanks! |
||
vmax = float(self.vmax) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should |
||
|
||
|
@@ -1070,7 +1083,7 @@ def __call__(self, value, clip=None): | |
resultnorm = result.copy() | ||
mask_over = result > vmax | ||
mask_under = result < vmin | ||
mask = (result >= vmin) * (result <= vmax) | ||
mask = ~(mask_over | mask_under) | ||
# Since the non linear function is arbitrary and may not be | ||
# defined outside the boundaries, we just set obvious under | ||
# and over values | ||
|
@@ -1079,72 +1092,38 @@ def __call__(self, value, clip=None): | |
resultnorm[mask] = (self._f(result[mask]) - self._f(vmin)) / \ | ||
(self._f(vmax) - self._f(vmin)) | ||
|
||
return np.ma.array(resultnorm) | ||
resultnorm = np.ma.array(resultnorm) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think this is necessary, because |
||
if is_scalar: | ||
return resultnorm[0] | ||
else: | ||
return resultnorm | ||
|
||
def inverse(self, value): | ||
""" | ||
Performs the inverse normalization from the `[0.0, 1.0]` into the | ||
`[vmin, vmax]` interval and returns it. | ||
Performs the inverse normalization from the ``[0.0, 1.0]`` into the | ||
``[`vmin`, `vmax`]`` interval and returns it. | ||
|
||
Parameters | ||
---------- | ||
value : float or ndarray of floats | ||
Data in the `[0.0, 1.0]` interval. | ||
Data in the ``[0.0, 1.0]`` interval. | ||
|
||
Returns | ||
------- | ||
result : float or ndarray of floats | ||
Data before normalization. | ||
|
||
""" | ||
self._check_vmin_vmax() | ||
vmin = self.vmin | ||
vmax = self.vmax | ||
self._update_f(vmin, vmax) | ||
value = self._finv( | ||
value * (self._f(vmax) - self._f(vmin)) + self._f(vmin)) | ||
return value | ||
|
||
@staticmethod | ||
def _fun_normalizer(fun): | ||
if fun(0.) == 0. and fun(1.) == 1.: | ||
return fun | ||
elif fun(0.) == 0.: | ||
return (lambda x: fun(x) / fun(1.)) | ||
else: | ||
return (lambda x: (fun(x) - fun(0.)) / (fun(1.) - fun(0.))) | ||
|
||
def autoscale(self, A): | ||
""" | ||
Autoscales the normalization based on the maximum and minimum values | ||
of `A`. | ||
|
||
Parameters | ||
---------- | ||
A : ndarray or maskedarray | ||
Array used to calculate the maximum and minimum values. | ||
|
||
""" | ||
self.vmin = float(np.ma.min(A)) | ||
self.vmax = float(np.ma.max(A)) | ||
|
||
def autoscale_None(self, A): | ||
""" | ||
Autoscales the normalization based on the maximum and minimum values | ||
of `A`, only if the limits were not already set. | ||
|
||
Parameters | ||
---------- | ||
A : ndarray or maskedarray | ||
Array used to calculate the maximum and minimum values. | ||
|
||
""" | ||
if self.vmin is None: | ||
self.vmin = float(np.ma.min(A)) | ||
if self.vmax is None: | ||
self.vmax = float(np.ma.max(A)) | ||
self.vmin = float(self.vmin) | ||
self.vmax = float(self.vmax) | ||
if self.vmin > self.vmax: | ||
def _check_vmin_vmax(self): | ||
if self.vmin >= self.vmax: | ||
raise ValueError("vmin must be smaller than vmax") | ||
|
||
def ticks(self, nticks=13): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not sure about the mixing of concerns here, but I'll leave that to @efiring to determine. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, I also was not sure of this, because technically vmin, and vmax, do not belong to this class. Actually the only thing my autoscale methods do differently is to convert to float, so maybe I should just make a tiny change to the autoscale methods of Normalize to resemble this: Methods in Normalize: def autoscale(self, A):
self.vmin = np.ma.min(A)
self.vmax = np.ma.max(A)
def autoscale_None(self, A):
' autoscale only None-valued vmin or vmax'
if self.vmin is None and np.size(A) > 0:
self.vmin = np.ma.min(A)
if self.vmax is None and np.size(A) > 0:
self.vmax = np.ma.max(A) Methods in FuncNorm: def autoscale(self, A):
self.vmin = float(np.ma.min(A))
self.vmax = float(np.ma.max(A))
def autoscale_None(self, A):
if self.vmin is None:
self.vmin = float(np.ma.min(A))
if self.vmax is None:
self.vmax = float(np.ma.max(A))
self.vmin = float(self.vmin)
self.vmax = float(self.vmax)
if self.vmin > self.vmax:
raise ValueError("vmin must be smaller than vmax") @efiring would it be ok, to include those changes (casting to float and vmax>vmin check) in Normalize, and remove the methods from FuncNorm? |
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can just start the docstring here, and all norms are functions that map data into colorspace, so you need to be more specific.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No, the first line should be a "short" description (in fact, it should be one-line, but we really don't enforce it that often.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I forgot to address this in my previous commit. What about saying:
The normalizer will use a provided custom function to map the data values into colormap values in the [0,1] range.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry @QuLogic :/ & sounds good @alvarosg
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would start the docstring (which is describing a class) with "A norm based on a monotonic function". Then a blank line, followed by the second sentence of the present init docstring, followed by the remainder of the present init docstring (Parameters, etc.). This is in accord with the numpydoc specification for classes: the init args and kwargs are described in the class docstring, and there is no need for an init docstring at all.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@efiring Thanks for pointing that out, I always thought the init could either be documented in the class, on in the init itself. I will do it the way you suggested.