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
Fixed incorrect string representation of floats in Cards #14508
Conversation
Thank you for your contribution to Astropy! 🌌 This checklist is meant to remind the package maintainers who will review this pull request of some common things to look for.
|
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 think we may need upper case. Some other comments that are less directly related to your PR.
astropy/io/fits/card.py
Outdated
if "." not in value_str and "E" not in value_str: | ||
value_str += ".0" | ||
elif "E" in value_str: | ||
value_str = str(value) |
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 think we may have to do str(value).upper()
to ensure we still have an E
- that may well be FITS standard.
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've changed the if
and .split()
to check for "e"
instead of "E"
since that is how python represents the exponent. I believe the upper case E
is indeed part of the FITS standard, however that is not an issue since I did not change the following line:
astropy/astropy/io/fits/card.py
Line 1314 in 60835aa
value_str = f"{significand}E{sign}{int(exponent):02d}" |
This will force the end result to always use the upper case E
.
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.
Good point. So, then it is more relevant if we actually can remove that whole stanza (which I think is likely to be the case).
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.
Sounds a bit risky but we can give it a try. I notice that the code specifically has int(exponent)
, but I don't think it's possible that Python ever returns a fractional exponent (1e0.5
isn't even legal syntax). Also, 1e-9
is converted to "1e-09"
so it seems Python automatically pads to at least 2 digits. The only way this would fail is if indeed the representation is different in different platforms.
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.
This part looks very sketchy to me:
astropy/astropy/io/fits/card.py
Lines 1315 to 1316 in 60835aa
elif "." not in value_str: | |
value_str += ".0" |
The only way this if
statement would be called is if a integer reached this point, but I don't think that could ever happen because we only call this function in these places:
astropy/astropy/io/fits/card.py
Lines 1287 to 1292 in 60835aa
elif isinstance(value, (float, np.floating)): | |
return f"{_format_float(value):>20}" | |
elif isinstance(value, (complex, np.complexfloating)): | |
val_str = f"({_format_float(value.real)}, {_format_float(value.imag)})" | |
return f"{val_str:>20}" |
I've checked and using both Python built-in complex numbers and numpy's complex numbers, .real
and .imag
are float even if we use 1j+1
. In other words, I don't see how we could get a number that does not have a decimal point while also not using an exponent.
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.
Agreed, that can be removed too! It was needed only with the format string:
In [2]: f"{5.0:16G}"
Out[2]: ' 5'
astropy/io/fits/card.py
Outdated
elif "E" in value_str: | ||
value_str = str(value) | ||
|
||
if "e" in value_str: | ||
# On some Windows builds of Python (and possibly other platforms?) the |
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 doubt this is still true.
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.
Do you mean the comments regarding the Windows builds of Python?
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.
Yes, I think python now no longer relies on anything OS-dependent for typesetting float values (especially since we no longer use the format string).
Does anyone know why Read the Docs does not like me using " |
Co-authored-by: P. L. Lim <2090236+pllim@users.noreply.github.com>
I tried to be a bit more aggressive with the changes and only truncate if we are using a scientific notation. I assumed that the string representation of floats in Python couldn't go over 20 characters in instances where scientific notation isn't used. However, this is apparently not true (for example, |
Probably best to leave the truncation, rounding for another time. Sounds like standard notation can give 22 characters, negating your second example:
We could only make this 21 by removing the leading 0, so not a real solution. |
Sorry, maybe my comment wasn't clear. What I meant is that from my testing I figured out that floats can surpass 20 chars even when not using scientific notation (which I initially didn't think possible). My question now is: is there a float for which Python gives a string representation that is 1) standard notation (not scientific), 2) more than 20 characters long and 3) has just a single decimal place? Because if such a float exists, the truncation code would break, since it would delete the decimal places, making it an invalid float in FITS. |
@kYwzor - I think the truncation itself is safe, just (slightly) wrong. Probably best to keep that for another PR, as you suggested earlier... |
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.
This looks all good to me - let's handle the truncation separately.
Approving now, but will wait with merging to give @saimn a chance to have a look.
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.
This broke some tests downstream in spectral-cube because we're using |
OK I think I see the problem - the error noted in #14507 is possibly not a bug. It is not possible to round-trip WCS->header->WCS with this PR in place. However, I can't produce a MWE; I don't know exactly what |
MWE: from astropy.wcs import WCS
ww = WCS(naxis=1)
ww.wcs.cdelt[0] = 1288.2149687900003
ww2 = WCS(ww.to_header())
ww.wcs.compare(ww.wcs) # True
ww.wcs.compare(ww2.wcs) # False |
Could we please add the above example as a regression test and revert this until we come up with a solution? |
To expand a bit: it looks like we have a problem with round-tripping from header->wcs->header identified in #14507, but the solution here breaks the round-trip from wcs->header->wcs. |
PR to revert at #14524 |
is below float64 resolution of 1e-15, so it may not be reasonable to expect this can roundtrip. In fact that example also fails in 5.2, even when setting
as the latter is created by
does not seem like it falls within the scope of this PR or the discussion to be had on https://github.com/astropy/astropy/pull/14508/files#r1133599288. |
So does it mean we need to revert or not? |
Ah, I'm afraid you're right, my MWE does not succeed on 5.2 either, it fails on both, so it is not a good MWE. The test that fails in spectral-cube has a little more going on in it; I thought I had narrowed it down to the right spot. I used |
@pllim we still do need to revert, I think - just because we haven't found where this breaks the spectral-cube test does not mean that this is a safe change. I'm finding it extremely difficult to produce a MWE that passes on 5.2.1 and fails on main, but spectral-cube's test definitely does. |
Are you sure it is this PR? |
Sure as I can be. I ran |
I'd rather see that better MWE first. Or you can also propose a revert PR from your fork. |
I've given up on the MWE. I have reproduced the path as precisely as possible and cannot get the MWE to pass with 5.2.1 and fail with main. I'm just changing the comparison to |
Ah, so it is like |
Yes; it is correct that that commit has changed the output of
which happened to just match the precision limitation of |
Alternatively you might still try the |
@keflavich it seems the spectral-cube bug is already introduced when reading the input file, which actually has
but is parsed (with any astropy version) as
Likely there is some floating point error introduced in |
Description
This pull request is to address a bug that caused
io.fits.Card
to format floats incorrectly, which could force the associated comments to be truncated.EDIT:
io.fits.Card
may use a string representation of floats that is larger than necessary #14507