You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Compression settings ignored when creating empty CompImageHDU #12216
When creating a HDU without specifying any data, the quantize_level and quantize_method arguments (at least) passed to the HDU class during initialisation are not honoured when data is later set via the HDU's data attribute. Initialising the HDU with even data=np.array([0.]) is enough to (seemingly) solve this issue.
Expected behavior
I would expect the compression settings passed as keyword parameters during initialisation to be remembered in the HDU object and then taken into account when the HDU's data is later updated by the user.
Actual behavior
It seems that if no data is passed during initialisation, the compression settings are assumed to be irrelevant and ignored. I think this should at least be documented (if not already).
Steps to Reproduce
importnumpyasnpfromastropy.ioimportfitstmpfile='test_fits.fits'defdiff(hdu):
# Generate the data to write to the FITS filenp.random.seed(0)
data=np.random.rand(50, 30).astype('float32')
# Set the actual datahdu.data=data.copy()
# Write compressed data to filehdul=fits.HDUList([fits.PrimaryHDU(), hdu])
hdul.writeto(tmpfile, overwrite=True)
# Print max abs. difference due to compressionhdul=fits.open(tmpfile)
print(np.abs(hdul[1].data-data).max())
diff(fits.CompImageHDU(quantize_level=-32))
diff(fits.CompImageHDU(quantize_level=-32, data=np.array([0.])))
kwargs= {'quantize_method': 2, 'dither_seed': 1}
diff(fits.CompImageHDU(**kwargs))
diff(fits.CompImageHDU(**kwargs, data=np.array([0.])))
Actual Values Returned
0.014079988
0.99980855
0.014079988
0.01334089
Expected Values Returned
0.99980855
0.99980855
0.01334089
0.01334089
i.e. initialising with data=None or data=np.array([0.]) should ultimately produce identical compressed values.
Welcome to Astropy 👋 and thank you for your first issue!
A project member will respond to you as soon as possible; in the meantime, please double-check the guidelines for submitting issues and make sure you've provided the requested details.
GitHub issues in the Astropy repository are used to track bug reports and feature requests; If your issue poses a question about how to use Astropy, please instead raise your question in the Astropy Discourse user forum and close this issue.
If you feel that this issue has not been responded to in a timely manner, please leave a comment mentioning our software support engineer @embray, or send a message directly to the development mailing list. If the issue is urgent or sensitive in nature (e.g., a security vulnerability) please send an e-mail directly to the private e-mail [email protected].
This issue may be related to the _update_header_data method not being called when the data attribute is updated. #12058 may be a partial solution to this, however it may also be necessary to save the keyword parameters passed to __init__ and also pass them to _update_header_data every time the data setter method is called (i.e. data is updated).
Now, the important part is that self._image_header["BITPIX"] is set to 8 ("Character or unsigned binary integer") by default (i.e., if self.data is None), but it's set to -32 (resp. -64) if self.data is a numpy array with dtype float32 (resp. float64). It so happens that BITPIX < 0 (which I read as a proxy for "data is IEEE floating point") is a necessary condition in _image_header_to_bintable_header_and_coldefs for quantize_level to have any effect:
ifgenerated_image_header["BITPIX"] <0: # floating point image
Indeed, ensuring that the generated header stays in sync with self.data by calling _update_header_data (combined with a way to memorise user-provided options) in the property setter seems like a potential solution, however it gets more complicated and this approach doesn't work out of the box because self.data = ... is actually called before self.header is in a usable state, so it results in an exception. For reproducibility, I'll put here the exact patch I used for this experiment, the test script, and the traceback:
Traceback (most recent call last):
File "/Users/clm/dev/astropy-project/coordinated/astropy/bugs/12216/g.py", line 2, in <module>
hdu = fits.CompImageHDU()
^^^^^^^^^^^^^^^^^^^
File "/Users/clm/dev/astropy-project/coordinated/astropy/astropy/utils/decorators.py", line 604, in wrapperreturn function(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^
File "/Users/clm/dev/astropy-project/coordinated/astropy/astropy/io/fits/hdu/compressed/compressed.py", line 321, in __init__self.data = data
^^^^^^^^^
File "/Users/clm/dev/astropy-project/coordinated/astropy/astropy/utils/decorators.py", line 848, in __set__
ret =self.fset(obj, val)
^^^^^^^^^^^^^^^^^^^
File "/Users/clm/dev/astropy-project/coordinated/astropy/astropy/io/fits/hdu/compressed/compressed.py", line 596, in dataself._update_header_data(self.header)
^^^^^^^^^^^
File "/Users/clm/dev/astropy-project/coordinated/astropy/astropy/utils/decorators.py", line 837, in __get__
val =self.fget(obj)
^^^^^^^^^^^^^^
File "/Users/clm/dev/astropy-project/coordinated/astropy/astropy/io/fits/hdu/compressed/compressed.py", line 657, in headerself._image_header = _bintable_header_to_image_header(self._header)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/Users/clm/dev/astropy-project/coordinated/astropy/astropy/io/fits/hdu/compressed/header.py", line 400, in _bintable_header_to_image_header"BITPIX", bintable_header["ZBITPIX"], hcomments["ZBITPIX"], before=1~~~~~~~~~~~~~~~^^^^^^^^^^^
File "/Users/clm/dev/astropy-project/coordinated/astropy/astropy/io/fits/header.py", line 170, in __getitem__
card =self._cards[self._cardindex(key)]
^^^^^^^^^^^^^^^^^^^^
File "/Users/clm/dev/astropy-project/coordinated/astropy/astropy/io/fits/header.py", line 1726, in _cardindexraiseKeyError(f"Keyword {keyword!r} not found.")
KeyError: "Keyword 'ZBITPIX' not found."
It's still possible that the proposed approach is viable and just requires more work, but I think there might be other avenues worth considering
decouple this class' initialisation process (easier said than done)
make CompressedImageHDU.data immutable (this wouldn't be as simple as removing the property setter). I think this would require a deprecation period, at the end of which data would become a required parameter of CompressedImageHDU.__init__.
add an optional dtype argument to CompressedImageHDU.__init__, that'd be required if and only if data isn't provided. I anticipate this would be the simplest implementation but it would increase the class complexity instead of decreasing it, which doesn't meet our goals for maintainability.
I think the next step is to actually try to follow through with the initial approach, just to get a proof-of-concept working example that we can use to judge wether it's actually viable or not. I'll give it a try.
Actually, taking a slightly different approach, I think the issue can be fixed by just memorizing compression options and re-injecting them on each header update. I'll open a PR and see if it gets traction.
Activity
github-actions commentedon Sep 29, 2021
Welcome to Astropy 👋 and thank you for your first issue!
A project member will respond to you as soon as possible; in the meantime, please double-check the guidelines for submitting issues and make sure you've provided the requested details.
GitHub issues in the Astropy repository are used to track bug reports and feature requests; If your issue poses a question about how to use Astropy, please instead raise your question in the Astropy Discourse user forum and close this issue.
If you feel that this issue has not been responded to in a timely manner, please leave a comment mentioning our software support engineer @embray, or send a message directly to the development mailing list. If the issue is urgent or sensitive in nature (e.g., a security vulnerability) please send an e-mail directly to the private e-mail [email protected].
ConorMacBride commentedon Sep 30, 2021
This issue may be related to the
_update_header_datamethod not being called when thedataattribute is updated. #12058 may be a partial solution to this, however it may also be necessary to save the keyword parameters passed to__init__and also pass them to_update_header_dataevery time thedatasetter method is called (i.e. data is updated).neutrinoceros commentedon Feb 9, 2024
I'm trying to dig into this. I'll start with a summary of my initial findings
I came to the same conclusion. To give a little bit more details:
_update_header_datais indeed called whenself.datais initialised:astropy/astropy/io/fits/hdu/compressed/compressed.py
Lines 321 to 339 in b299eb5
astropy/astropy/io/fits/hdu/compressed/compressed.py
Lines 519 to 534 in b299eb5
and it's apparent that this operation depends on the state of
self.data:astropy/astropy/io/fits/hdu/compressed/compressed.py
Lines 496 to 497 in b299eb5
Now, the important part is that
self._image_header["BITPIX"]is set to8("Character or unsigned binary integer") by default (i.e., ifself.data is None), but it's set to-32(resp.-64) ifself.datais a numpy array with dtypefloat32(resp.float64). It so happens thatBITPIX < 0(which I read as a proxy for "data is IEEE floating point") is a necessary condition in_image_header_to_bintable_header_and_coldefsforquantize_levelto have any effect:astropy/astropy/io/fits/hdu/compressed/header.py
Line 820 in b299eb5
Indeed, ensuring that the generated header stays in sync with
self.databy calling_update_header_data(combined with a way to memorise user-provided options) in the property setter seems like a potential solution, however it gets more complicated and this approach doesn't work out of the box becauseself.data = ...is actually called beforeself.headeris in a usable state, so it results in an exception. For reproducibility, I'll put here the exact patch I used for this experiment, the test script, and the traceback:Naive approach
neutrinoceros commentedon Feb 9, 2024
It's still possible that the proposed approach is viable and just requires more work, but I think there might be other avenues worth considering
CompressedImageHDU.dataimmutable (this wouldn't be as simple as removing the property setter). I think this would require a deprecation period, at the end of whichdatawould become a required parameter ofCompressedImageHDU.__init__.dtypeargument toCompressedImageHDU.__init__, that'd be required if and only ifdataisn't provided. I anticipate this would be the simplest implementation but it would increase the class complexity instead of decreasing it, which doesn't meet our goals for maintainability.I think the next step is to actually try to follow through with the initial approach, just to get a proof-of-concept working example that we can use to judge wether it's actually viable or not. I'll give it a try.
neutrinoceros commentedon Feb 9, 2024
Actually, taking a slightly different approach, I think the issue can be fixed by just memorizing compression options and re-injecting them on each header update. I'll open a PR and see if it gets traction.
TST: add regression tests for issue astropy#12216
TST: add regression tests for issue astropy#12216
TST: add regression tests for issue astropy#12216
Merge pull request #16019 from neutrinoceros/io/fits/bug/remember_com…
TST: add regression tests for issue astropy#12216