Skip to content

Fix crash when imshow encounters longdouble data #10768

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

Merged
merged 1 commit into from
Mar 15, 2018

Conversation

br-Zhang
Copy link
Contributor

@br-Zhang br-Zhang commented Mar 13, 2018

PR Summary

This PR solves #10342. Support for longdouble was added in #8447, however attempting to draw anything causes a crash. This PR adds a cast to float64 when imshow encounters longdouble data.

PR Checklist

  • Has Pytest style unit tests
  • Code is PEP 8 compliant
  • New features are documented, with examples if plot related
  • Documentation is sphinx and numpydoc compliant
  • Added an entry to doc/users/next_whats_new/ if major new feature (follow instructions in README.rst there)
  • Documented in doc/api/api_changes.rst if API changed in a backward-incompatible way

@br-Zhang br-Zhang changed the title Fix crash when imshow encouters longdouble data Fix crash when imshow encounters longdouble data Mar 13, 2018
@QuLogic QuLogic requested a review from anntzer March 13, 2018 03:43
@anntzer
Copy link
Contributor

anntzer commented Mar 13, 2018

So if I understand correctly, #8447 is essentially pointless? #8447 added downcasting of float128 to float64 at normalization time, but we always run interpolation first and that step already needs to work on float64?

@br-Zhang
Copy link
Contributor Author

I believe that is correct.

@jklymak
Copy link
Member

jklymak commented Mar 13, 2018

If we just down-cast to float64, that's not really supporting float128, its just not crashing. I think that float128 should error, maybe w/ a better error, and tell users they need to cast themselves, given that opinions vary wildly as to how down-casting should happen. ping @efiring who had an opinion about all this when I added float128 before.

@efiring
Copy link
Member

efiring commented Mar 13, 2018

Maybe downcast with a warning. Effectively, as a plotting library, we are always downcasting drastically, since plots have very limited combinations of dynamic range and resolution. I presume the use of float128 is very rare, and I don't see any way that "supporting" it in matplotlib is going to help anyone do anything useful. Use of float128 should be very rare because it is wildly platform-dependent, and doesn't even provide the advertised 128 bits. It is not included in the numpy table of supported types:
https://docs.scipy.org/doc/numpy/user/basics.types.html
The situation is explained at the bottom of that section:
https://docs.scipy.org/doc/numpy/user/basics.types.html#extended-precision
Note that there is also a float96 which should be handled in the same (minimal) way as float128.

@jklymak
Copy link
Member

jklymak commented Mar 13, 2018

I'd be fine with downcast-with-warning... For this PR, you'd want to add the warning, and an assert that its emited in the test...

@anntzer
Copy link
Contributor

anntzer commented Mar 13, 2018

Probably also worth (effectively) reverting #8447 then, given that by the time we reach colormapping, the data is certainly not float128 anymore? (dunno)

@br-Zhang
Copy link
Contributor Author

br-Zhang commented Mar 13, 2018

I've updated the pull request to add a warning before casting, remove the redundant check for casting in colors.py, and added an assert in the test to ensure that the warning is emitted.

@@ -361,6 +362,12 @@ def _make_image(self, A, in_bbox, out_bbox, clip_bbox, magnification=1.0,
a_min, a_max = np.int32(0), np.int32(1)
if inp_dtype.kind == 'f':
scaled_dtype = A.dtype
# Cast to float64
if A.dtype == np.longdouble:
Copy link
Member

Choose a reason for hiding this comment

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

OK, still not sure what to do about np.float128 and np.float96. Should these be trapped here as well?

Copy link
Contributor Author

@br-Zhang br-Zhang Mar 14, 2018

Choose a reason for hiding this comment

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

Based on the platform version, np.float128 or np.float96 may or may not exist so adding traps for them might not work. Instead, I think casting anything that isn't float64, float32 or float16 to float64 would be a better idea.

Copy link
Member

Choose a reason for hiding this comment

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

Really, they aren't even defined? Yuck....

Copy link
Contributor Author

@br-Zhang br-Zhang Mar 14, 2018

Choose a reason for hiding this comment

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

Yes. np.longdouble is platform dependent also. That's why I most likely have to remove the assert for warnings from the test case since the CI runs on Windows which has np.longdouble == 'float64' == 'float'. This causes either excessive warnings which will break all sorts of CI tests (if I choose to show warnings for each cast) or no warnings at all (if I only show warnings on casts from a larger size to smaller).

@anntzer anntzer removed their request for review March 14, 2018 00:17
@jklymak
Copy link
Member

jklymak commented Mar 14, 2018

Given the issues with longdouble float128 etc maybe the right thing to do is let this error?

@br-Zhang
Copy link
Contributor Author

The problem with longdouble is mostly with testing. The changes in this PR makes a downcast to float64 whenever it encounters longdouble, float96 or float128, which is working fine. The issue is that, on Windows, longdouble == float64 so a trap/error for longdouble would cause an error on float/float64 too. Adding a check for float64 solves this but means that warnings will only be emitted on platforms where longdouble == float128/float96, which makes it hard to write a test that asserts for a warning.

@@ -361,6 +362,13 @@ def _make_image(self, A, in_bbox, out_bbox, clip_bbox, magnification=1.0,
a_min, a_max = np.int32(0), np.int32(1)
if inp_dtype.kind == 'f':
scaled_dtype = A.dtype
# Cast to float64
if not (A.dtype == np.float32 or A.dtype == np.float16):
Copy link
Member

Choose a reason for hiding this comment

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

if A.dtype not in (np.float32, np.float16):

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.

@@ -361,6 +362,13 @@ def _make_image(self, A, in_bbox, out_bbox, clip_bbox, magnification=1.0,
a_min, a_max = np.int32(0), np.int32(1)
if inp_dtype.kind == 'f':
scaled_dtype = A.dtype
# Cast to float64
if not (A.dtype == np.float32 or A.dtype == np.float16):
if (A.dtype != np.float64):
Copy link
Member

Choose a reason for hiding this comment

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

Remove unnecessary parentheses.

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.

@@ -361,6 +362,13 @@ def _make_image(self, A, in_bbox, out_bbox, clip_bbox, magnification=1.0,
a_min, a_max = np.int32(0), np.int32(1)
if inp_dtype.kind == 'f':
scaled_dtype = A.dtype
# Cast to float64
if A.dtype not in (np.float32, np.float16):
if A.dtype != np.float64:
Copy link
Member

Choose a reason for hiding this comment

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

might as well just make this one if-statement....

Copy link
Member

Choose a reason for hiding this comment

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

There are things happening after this one but not in it...

Copy link
Member

@jklymak jklymak Mar 14, 2018

Choose a reason for hiding this comment

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

if A.dtype not in (np.float32, np.float16, np.float64):
   warnings.warn()
   scaled_dtype = np.float64

should do it, right?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I actually did this in commit a453ba4. This led to problems in some Windows environments and failures in CI. It seems like scaled_dtype needs to be set to np.float64 instead of np.longdouble even if np.longdouble == np.float64.

Copy link
Member

Choose a reason for hiding this comment

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

not in returns something different than !=? I'm not following the logic here at all.

Copy link
Contributor Author

@br-Zhang br-Zhang Mar 14, 2018

Choose a reason for hiding this comment

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

It's very platform specific. Running on Win-64 gives:

>>> numpy.longdouble in (numpy.float16, numpy.float64)
False

Copy link
Member

Choose a reason for hiding this comment

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

But numpy.longdouble == numpy.float64 returns true?

Copy link
Contributor Author

@br-Zhang br-Zhang Mar 14, 2018

Choose a reason for hiding this comment

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

Very weird.

>>> numpy.longdouble == numpy.float64
False

Copy link
Member

Choose a reason for hiding this comment

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

Again, the problem doesn't have anything to do with the check here; it's the use of scaled_dtype later that causes a problem. If you flatten the condition, then scaled_dtype=np.longdouble is not replaced by np.float64 (on Windows only!) It's then crashing in C code later (which is arguably a NumPy inconsistency) which doesn't understand np.longdouble.

Copy link
Member

Choose a reason for hiding this comment

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

OK; I get it now. everything not np.float16/32 has to be explicitly set to np.float64, even if python thinks np.longdouble is equivalent to np.float64, because the C code doesn't think its equivalent.

Sorry to be so dense, but I do think its pretty bad that the C code ends up incompatible in this way.

Copy link
Member

@QuLogic QuLogic left a comment

Choose a reason for hiding this comment

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

This should be squashed.

@@ -10,6 +10,7 @@
from io import BytesIO

from math import ceil
import warnings
Copy link
Member

Choose a reason for hiding this comment

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

This belongs after logging.

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

@br-Zhang br-Zhang force-pushed the bugfix-for-issue-10342 branch from cc74d0b to 809353c Compare March 15, 2018 01:44
@br-Zhang
Copy link
Contributor Author

Squashed all the previous commits into one commit.

@jklymak jklymak added this to the v3.0 milestone Mar 15, 2018
@jklymak
Copy link
Member

jklymak commented Mar 15, 2018

I'll merge tomorrow if no one does so earlier. Just want to leave it up if anyone else wants to chime in. But I think I understand what is going on now and this fix seems fine.

@jklymak jklymak merged commit f49c7b3 into matplotlib:master Mar 15, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants