Skip to content

Navigation Menu

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

BUG: fix normalizing image data contained in np.ndarray subclass #27682

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

Open
wants to merge 1 commit into
base: main
Choose a base branch
Loading
from

Conversation

neutrinoceros
Copy link
Contributor

PR summary

This fixes a downstream issue with plotting data from a astropy.units.Quantity object (which derives from np.ndarray), namely astropy/astropy#11306

Although the external issue was also linked to #19476, this patch does not fix that problem.
As a result, I am unsure how to write a test for the astropy bug here, though I could easily add a test in astropy itself (which is regularily tested against matplotlib dev).

PR checklist

@story645
Copy link
Member

story645 commented Jan 22, 2024

As a result, I am unsure how to write a test for the astropy bug here, though I could easily add a test in astropy itself (which is regularily tested against matplotlib dev).

Can you create a fake unit that mimics the astropy behavior? Something like a very thin wrapper over nd.array that triggers the error but is fixed here? Something like #26953

@neutrinoceros
Copy link
Contributor Author

Thank you for your suggestion ! I wasn't able to construct such a mock with a bottom-up approach (build one from the ground up), supposedly because of the sheer complexity of astropy.units.Quantity, so I'm now trying a top-down approach (take astropy and iteratively remove parts that don't play a role in the error). It's taking a while but I'm getting there.

@story645
Copy link
Member

so I'm now trying a top-down approach (take astropy and iteratively remove parts that don't play a role in the error). It's taking a while but I'm getting there.

Is much appreciated since that helps us better understand what's going wonky here.

@neutrinoceros neutrinoceros force-pushed the robust_lim_comparison branch 3 times, most recently from eed2fe8 to d337f69 Compare January 22, 2024 19:28
@neutrinoceros
Copy link
Contributor Author

Ok I think it's now ready for review.

@neutrinoceros neutrinoceros marked this pull request as ready for review January 22, 2024 19:57
@pllim
Copy link

pllim commented Jan 22, 2024

Thanks!

Re: mocking astropy.units.Quantity -- @mhvk can double check if needed.

@rcomer
Copy link
Member

rcomer commented Jan 22, 2024

Apologies for asking this several hours late, but are either of these relevant here?


class QuantityND(np.ndarray):

@pllim
Copy link

pllim commented Jan 22, 2024

@rcomer , not sure. I tracked that to #9049 and only pint was mentioned, not astropy.units.

@pllim
Copy link

pllim commented Jan 22, 2024

And the second one to #18286 that mentioned unyt and not astropy.units.

@neutrinoceros
Copy link
Contributor Author

@rcomer Indeed these classes will not reproduce the issue we're currently seeing with astropy.units.Quantity. I think I may have used too broad a title for this PR, but it's really about some corner case specific to astropy.units.Quantity. For instance, astropy.units.Quantity intentionally overrides the ndarray.item method so that it doesn't return numerical scalars, but if I remove def item(...) from my reduced mock, the test passes without a patch.

Copy link
Contributor

@mhvk mhvk left a comment

Choose a reason for hiding this comment

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

Certainly looks like a reasonable mock-up of Quantity!

@@ -461,12 +461,12 @@ def _make_image(self, A, in_bbox, out_bbox, clip_bbox, magnification=1.0,
vmid = np.float64(self.norm.vmin) + dv / 2
fact = 1e7 if scaled_dtype == np.float64 else 1e4
newmin = vmid - dv * fact
if newmin < a_min:
if newmin < np.float64(a_min):
Copy link
Contributor

Choose a reason for hiding this comment

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

I'd maybe do this just below where A itself is converted to a plain array, ie.,

A_scaled = np.array(A, dtype=scaled_dtype)
a_min = np.float64(a_min)
a_max = np.float64(a_max)

(Arguably, scaled_dtype.type might be more logical than np.float64, but then one should adjust other things too)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done

@story645
Copy link
Member

but if I remove def item(...) from my reduced mock, the test passes without a patch.

Can you add a comment that that's the important bit?

@neutrinoceros
Copy link
Contributor Author

Actually I think every part of the mock class is crucial to reproduce the problem, should I really comment all of them ?
In the real-life use case (with astropy.units.Quantity), the error we see is raised through three nested try/except blocks 🙃

@story645
Copy link
Member

In the real-life use case (with astropy.units.Quantity), the error we see is raised through three nested try/except blocks

😬 No, if it's a combination of the things then yeah it doesn't need a specific comment.

@dstansby
Copy link
Member

I've been trying to understand what's going on here. It seems:

  • _make_image(A, ...) is given A as a unitful array
  • a_min = A.min() / a_max = A.max() therefore have units
  • so the fix in this PR is to just strip the units by calling np.float64

I think the better long term fix here is to add proper support for the image data to have units in the _ImageBase class. This would provide a path to solving other bugs and feature requests around images with units, e.g. #25062, #19476.

So it's unclear to me whether it's worth merging this PR as a bit of a stopgap fix, or whether we should be aiming to add proper support for unitful data in _ImageBase instead. It would be good to hear what @ksunden thinks on this front from a Matplotlib/units point of view?

@dstansby
Copy link
Member

dstansby commented Jan 24, 2024

As a quick proof of concept, the following diff fixes the original issue, also allows mousing over the data to work (that's still broken with this PR), and opens the door to further improvements like labelling the colorbar with the units.

diff --git a/lib/matplotlib/image.py b/lib/matplotlib/image.py
index e326906269..88325a59ed 100644
--- a/lib/matplotlib/image.py
+++ b/lib/matplotlib/image.py
@@ -26,6 +26,7 @@ import matplotlib.colors as mcolors
 from matplotlib.transforms import (
     Affine2D, BboxBase, Bbox, BboxTransform, BboxTransformTo,
     IdentityTransform, TransformedBbox)
+import matplotlib.units as munits
 
 _log = logging.getLogger(__name__)
 
@@ -724,6 +725,13 @@ class _ImageBase(martist.Artist, cm.ScalarMappable):
         ----------
         A : array-like or `PIL.Image.Image`
         """
+        converter = munits.registry.get_converter(A)
+        if converter is not None:
+            self._units = converter.default_units(A, self)
+            A = converter.convert(A, self._units, self)
+        else:
+            self._units = None
+
         if isinstance(A, PIL.Image.Image):
             A = pil_to_array(A)  # Needed e.g. to apply png palette.
         self._A = self._normalize_image_array(A)

It does have the issue that the last arguments to the converter methods are meant to be of type Axis and not _ImageBase though, but in reality I'm not sure that's a huge issue...

@neutrinoceros
Copy link
Contributor Author

Happy to drop this PR in favour of a better fix if needed !

@jklymak
Copy link
Member

jklymak commented Jan 25, 2024

We don't have converter logic for data passed to color mapping. Folks are asked to pass a numpy array to scalar mappables. Maybe its OK to normalize those mappable to so something without units earlier, but I don't know that we have enough infrastructure in place to actually use unit converters here.

@story645
Copy link
Member

story645 commented Jan 25, 2024

but I don't know that we have enough infrastructure in place to actually use unit converters here.

I still think that throwing a convertor on NoNorm (or possibly writing a UnitNorm() if we want to avoid any magic) might do the trick?

I think the complicated part has always been propagating the correct labels back to the colorbar.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants
Morty Proxy This is a proxified and sanitized view of the page, visit original site.