-
-
Notifications
You must be signed in to change notification settings - Fork 8k
MultiNorm class #29876
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
base: main
Are you sure you want to change the base?
MultiNorm class #29876
Changes from all commits
e4f179f
a57146b
c6cf321
babbee0
8a59948
270cb64
da1ac73
347df5b
22e9093
81372c6
f1cdc10
e6d59ed
2d9d00c
68c5527
aa99db3
f61c06b
c0a0d6d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -32,6 +32,7 @@ Color norms | |
PowerNorm | ||
SymLogNorm | ||
TwoSlopeNorm | ||
MultiNorm | ||
|
||
Univariate Colormaps | ||
-------------------- | ||
|
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -2337,6 +2337,17 @@ def _changed(self): | |||||
""" | ||||||
self.callbacks.process('changed') | ||||||
|
||||||
@property | ||||||
@abstractmethod | ||||||
def n_components(self): | ||||||
""" | ||||||
The number of normalized components. | ||||||
This is number of elements of the parameter to ``__call__`` and of | ||||||
*vmin*, *vmax*. | ||||||
""" | ||||||
pass | ||||||
|
||||||
|
||||||
class Normalize(Norm): | ||||||
""" | ||||||
|
@@ -2547,6 +2558,19 @@ def scaled(self): | |||||
# docstring inherited | ||||||
return self.vmin is not None and self.vmax is not None | ||||||
|
||||||
@property | ||||||
def n_components(self): | ||||||
""" | ||||||
The number of distinct components supported (1). | ||||||
This is number of elements of the parameter to ``__call__`` and of | ||||||
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.
Suggested change
|
||||||
*vmin*, *vmax*. | ||||||
This class support only a single component, as opposed to `MultiNorm` | ||||||
which supports multiple components. | ||||||
""" | ||||||
return 1 | ||||||
|
||||||
|
||||||
class TwoSlopeNorm(Normalize): | ||||||
def __init__(self, vcenter, vmin=None, vmax=None): | ||||||
|
@@ -3272,6 +3296,300 @@ def inverse(self, value): | |||||
return value | ||||||
|
||||||
|
||||||
class MultiNorm(Norm): | ||||||
""" | ||||||
A class which contains multiple scalar norms. | ||||||
""" | ||||||
|
||||||
def __init__(self, norms, vmin=None, vmax=None, clip=None): | ||||||
""" | ||||||
Parameters | ||||||
---------- | ||||||
norms : list of (str or `Normalize`) | ||||||
The constituent norms. The list must have a minimum length of 1. | ||||||
vmin, vmax : None or list of (float or None) | ||||||
Limits of the constituent norms. | ||||||
If a list, one value is assigned to each of the constituent | ||||||
norms. | ||||||
If None, the limits of the constituent norms | ||||||
are not changed. | ||||||
clip : None or list of bools, default: None | ||||||
Determines the behavior for mapping values outside the range | ||||||
``[vmin, vmax]`` for the constituent norms. | ||||||
If a list, each value is assigned to each of the constituent | ||||||
norms. | ||||||
If None, the behaviour of the constituent norms is not changed. | ||||||
""" | ||||||
if cbook.is_scalar_or_string(norms): | ||||||
raise ValueError( | ||||||
"MultiNorm must be assigned an iterable of norms, where each " | ||||||
f"norm is of type `str`, or `Normalize`, not {type(norms)}") | ||||||
|
||||||
if len(norms) < 1: | ||||||
raise ValueError("MultiNorm must be assigned at least one norm") | ||||||
|
||||||
def resolve(norm): | ||||||
if isinstance(norm, str): | ||||||
scale_cls = _api.check_getitem(scale._scale_mapping, norm=norm) | ||||||
return mpl.colorizer._auto_norm_from_scale(scale_cls)() | ||||||
elif isinstance(norm, Normalize): | ||||||
return norm | ||||||
else: | ||||||
raise ValueError( | ||||||
"Each norm assgned to MultiNorm must be " | ||||||
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.
Suggested change
|
||||||
f"of type `str`, or `Normalize`, not {type(norm)}") | ||||||
|
||||||
self._norms = tuple(resolve(norm) for norm in norms) | ||||||
|
||||||
self.callbacks = cbook.CallbackRegistry(signals=["changed"]) | ||||||
|
||||||
self.vmin = vmin | ||||||
self.vmax = vmax | ||||||
self.clip = clip | ||||||
|
||||||
for n in self._norms: | ||||||
n.callbacks.connect('changed', self._changed) | ||||||
|
||||||
@property | ||||||
def n_components(self): | ||||||
"""Number of norms held by this `MultiNorm`.""" | ||||||
return len(self._norms) | ||||||
|
||||||
@property | ||||||
def norms(self): | ||||||
"""The individual norms held by this `MultiNorm`.""" | ||||||
return self._norms | ||||||
|
||||||
@property | ||||||
def vmin(self): | ||||||
"""The lower limit of each constituent norm.""" | ||||||
return tuple(n.vmin for n in self._norms) | ||||||
|
||||||
@vmin.setter | ||||||
def vmin(self, values): | ||||||
if values is None: | ||||||
return | ||||||
if not np.iterable(values) or len(values) != self.n_components: | ||||||
raise ValueError("*vmin* must have one component for each norm. " | ||||||
f"Expected an iterable of length {self.n_components}, " | ||||||
f"but got {values!r}") | ||||||
with self.callbacks.blocked(): | ||||||
for norm, v in zip(self.norms, values): | ||||||
norm.vmin = v | ||||||
self._changed() | ||||||
|
||||||
@property | ||||||
def vmax(self): | ||||||
"""The upper limit of each constituent norm.""" | ||||||
return tuple(n.vmax for n in self._norms) | ||||||
|
||||||
@vmax.setter | ||||||
def vmax(self, values): | ||||||
if values is None: | ||||||
return | ||||||
if not np.iterable(values) or len(values) != self.n_components: | ||||||
raise ValueError("*vmax* must have one component for each norm. " | ||||||
f"Expected an iterable of length {self.n_components}, " | ||||||
f"but got {values!r}") | ||||||
with self.callbacks.blocked(): | ||||||
for norm, v in zip(self.norms, values): | ||||||
norm.vmax = v | ||||||
self._changed() | ||||||
|
||||||
@property | ||||||
def clip(self): | ||||||
"""The clip behaviour of each constituent norm.""" | ||||||
return tuple(n.clip for n in self._norms) | ||||||
|
||||||
@clip.setter | ||||||
def clip(self, values): | ||||||
if values is None: | ||||||
return | ||||||
if not np.iterable(values) or len(values) != self.n_components: | ||||||
raise ValueError("*clip* must have one component for each norm. " | ||||||
f"Expected an iterable of length {self.n_components}, " | ||||||
f"but got {values!r}") | ||||||
with self.callbacks.blocked(): | ||||||
for norm, v in zip(self.norms, values): | ||||||
norm.clip = v | ||||||
self._changed() | ||||||
|
||||||
def _changed(self): | ||||||
""" | ||||||
Call this whenever the norm is changed to notify all the | ||||||
callback listeners to the 'changed' signal. | ||||||
""" | ||||||
self.callbacks.process('changed') | ||||||
|
||||||
def __call__(self, values, clip=None): | ||||||
""" | ||||||
Normalize the data and return the normalized data. | ||||||
Each component of the input is normalized via the constituent norm. | ||||||
Parameters | ||||||
---------- | ||||||
values : array-like | ||||||
The input data, as an iterable or a structured numpy array. | ||||||
- If iterable, must be of length `n_components`. Each element can be a | ||||||
scalar or array-like and is normalized through the correspong norm. | ||||||
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.
Suggested change
|
||||||
- If structured array, must have `n_components` fields. Each field | ||||||
is normalized through the corresponding norm. | ||||||
clip : list of bools or None, optional | ||||||
Determines the behavior for mapping values outside the range | ||||||
``[vmin, vmax]``. See the description of the parameter *clip* in | ||||||
`.Normalize`. | ||||||
If ``None``, defaults to ``self.clip`` (which defaults to | ||||||
``False``). | ||||||
Returns | ||||||
------- | ||||||
tuple | ||||||
Normalized input values | ||||||
Notes | ||||||
----- | ||||||
If not already initialized, ``self.vmin`` and ``self.vmax`` are | ||||||
initialized using ``self.autoscale_None(values)``. | ||||||
""" | ||||||
if clip is None: | ||||||
clip = self.clip | ||||||
if not np.iterable(clip) or len(clip) != self.n_components: | ||||||
raise ValueError("*clip* must have one component for each norm. " | ||||||
f"Expected an iterable of length {self.n_components}, " | ||||||
f"but got {clip!r}") | ||||||
|
||||||
values = self._iterable_components_in_data(values, self.n_components) | ||||||
result = tuple(n(v, clip=c) for n, v, c in zip(self.norms, values, clip)) | ||||||
return result | ||||||
|
||||||
def inverse(self, values): | ||||||
""" | ||||||
Map the normalized values (i.e., index in the colormap) back to data values. | ||||||
Parameters | ||||||
---------- | ||||||
values : array-like | ||||||
The input data, as an iterable or a structured numpy array. | ||||||
- If iterable, must be of length `n_components`. Each element can be a | ||||||
scalar or array-like and is mapped through the correspong norm. | ||||||
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.
Suggested change
|
||||||
- If structured array, must have `n_components` fields. Each field | ||||||
is mapped through the the corresponding norm. | ||||||
""" | ||||||
values = self._iterable_components_in_data(values, self.n_components) | ||||||
result = [n.inverse(v) for n, v in zip(self.norms, values)] | ||||||
return result | ||||||
|
||||||
def autoscale(self, A): | ||||||
""" | ||||||
For each constituent norm, set *vmin*, *vmax* to min, max of the corresponding | ||||||
component in *A*. | ||||||
Parameters | ||||||
---------- | ||||||
A : array-like | ||||||
The input data, as an iterable or a structured numpy array. | ||||||
- If iterable, must be of length `n_components`. Each element | ||||||
is used for the limits of one constituent norm. | ||||||
- If structured array, must have `n_components` fields. Each field | ||||||
is used for the limits of one constituent norm. | ||||||
""" | ||||||
with self.callbacks.blocked(): | ||||||
A = self._iterable_components_in_data(A, self.n_components) | ||||||
for n, a in zip(self.norms, A): | ||||||
n.autoscale(a) | ||||||
self._changed() | ||||||
|
||||||
def autoscale_None(self, A): | ||||||
QuLogic marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
""" | ||||||
If *vmin* or *vmax* are not set on any constituent norm, | ||||||
use the min/max of the corresponding component in *A* to set them. | ||||||
Parameters | ||||||
---------- | ||||||
A : array-like | ||||||
The input data, as an iterable or a structured numpy array. | ||||||
- If iterable, must be of length `n_components`. Each element | ||||||
is used for the limits of one constituent norm. | ||||||
- If structured array, must have `n_components` fields. Each field | ||||||
is used for the limits of one constituent norm. | ||||||
""" | ||||||
with self.callbacks.blocked(): | ||||||
A = self._iterable_components_in_data(A, self.n_components) | ||||||
for n, a in zip(self.norms, A): | ||||||
n.autoscale_None(a) | ||||||
self._changed() | ||||||
|
||||||
def scaled(self): | ||||||
"""Return whether both *vmin* and *vmax* are set on all constituent norms.""" | ||||||
trygvrad marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
return all(n.scaled() for n in self.norms) | ||||||
|
||||||
@staticmethod | ||||||
def _iterable_components_in_data(data, n_components): | ||||||
""" | ||||||
Provides an iterable over the components contained in the data. | ||||||
An input array with `n_components` fields is returned as a tuple of length n | ||||||
referencing slices of the original array. | ||||||
Parameters | ||||||
---------- | ||||||
data : array-like | ||||||
The input data, as an iterable or a structured numpy array. | ||||||
- If iterable, must be of length `n_components` | ||||||
- If structured array, must have `n_components` fields. | ||||||
Returns | ||||||
------- | ||||||
tuple of np.ndarray | ||||||
QuLogic marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
""" | ||||||
if isinstance(data, np.ndarray) and data.dtype.fields is not None: | ||||||
# structured array | ||||||
if len(data.dtype.fields) != n_components: | ||||||
raise ValueError( | ||||||
"Structured array inputs to MultiNorm must have the same " | ||||||
"number of fields as components in the MultiNorm. Expected " | ||||||
f"{n_components}, but got {len(data.dtype.fields)} fields" | ||||||
) | ||||||
else: | ||||||
return tuple(data[field] for field in data.dtype.names) | ||||||
try: | ||||||
n_elements = len(data) | ||||||
except TypeError: | ||||||
raise ValueError("MultiNorm expects a sequence with one element per " | ||||||
f"component as input, but got {data!r} instead") | ||||||
if n_elements != n_components: | ||||||
if isinstance(data, np.ndarray) and data.shape[-1] == n_components: | ||||||
if len(data.shape) == 2: | ||||||
raise ValueError( | ||||||
f"MultiNorm expects a sequence with one element per component. " | ||||||
"You can use `data_transposed = data.T`" | ||||||
"to convert the input data of shape " | ||||||
f"{data.shape} to a compatible shape {data.shape[::-1]} ") | ||||||
else: | ||||||
raise ValueError( | ||||||
f"MultiNorm expects a sequence with one element per component. " | ||||||
"You can use `data_as_list = [data[..., i] for i in " | ||||||
"range(data.shape[-1])]` to convert the input data of shape " | ||||||
f" {data.shape} to a compatible list") | ||||||
|
||||||
raise ValueError( | ||||||
"MultiNorm expects a sequence with one element per component. " | ||||||
f"This MultiNorm has {n_components} components, but got a sequence " | ||||||
f"with {n_elements} elements" | ||||||
) | ||||||
|
||||||
return tuple(data[i] for i in range(n_elements)) | ||||||
|
||||||
|
||||||
def rgb_to_hsv(arr): | ||||||
""" | ||||||
Convert an array of float RGB values (in the range [0, 1]) to HSV values. | ||||||
|
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.