Repository navigation
Description
While experimenting with checksums for compressed FITS images, I came across the following strange behavior. First, I load up the comp.fits file from our test suite and write it out, forcing a checksum calculation:
In [1]: import numpy as np
...: from astropy.io import fits
In [2]: from astropy.io.fits.hdu.base import _ValidHDU
...: _ValidHDU._get_timestamp = lambda self: "2013-12-20T13:36:10" # Ensure deterministic checksums
In [3]: from astropy.utils.data import get_pkg_data_filename
...:
In [4]: hdulist = fits.open(get_pkg_data_filename('data/comp.fits', package='astropy.io.fits.tests'))
...: hdulist.writeto('test_checksum.fits', overwrite=True, checksum=True)Then I can try and load it up as a raw binary table:
In [5]: bintable_hdu = fits.open('test_checksum.fits', disable_image_compression=True)[1]
In [6]: bintable_hdu.header['CHECKSUM']
Out[6]: 'ZeAbdb8aZbAabb7a'
In [7]: bintable_hdu.header['DATASUM']
Out[7]: '113055149'and I can also load it up decompressed:
In [8]: comp_image_hdu = fits.open('test_checksum.fits')[1]
In [9]: comp_image_hdu.header['CHECKSUM']
Out[9]: 'ZeAbdb8aZbAabb7a'
In [10]: comp_image_hdu.header['DATASUM']
Out[10]: '113055149'The checksums and datasums are the same, but they shouldn't be - the ones for bintable_hdu should be the ones for the actual binary table in the FITS file, while the comp_image_hdu ones should be the ones for the decompressed image.
Furthermore, the image checksums and datasums should be present in the binary table as ZHECKSUM and ZDATASUM but they are not.
It looks like the checksum and datasum for comp_image_hdu should actually be the following:
In [14]: image_hdu = fits.ImageHDU(comp_image_hdu.data, header=comp_image_hdu.header)
...: image_hdu.writeto('image.fits', overwrite=True, checksum=True)
In [15]: image_hdu.header['CHECKSUM']
Out[15]: 'D2GXD29VD2EVD29V'
In [16]: image_hdu.header['DATASUM']
Out[16]: '2189405276'I am currently working on a possible refactor of CompImageHDU and in my version I get the D2GXD29VD2EVD29V checksum, so I wanted to double check whether we agree that the current behavior is buggy and that the following test is incorrect:
def test_compressed_image_data(self):
with fits.open(self.data("comp.fits")) as h1:
h1.writeto(self.temp("tmp.fits"), overwrite=True, checksum=True)
with fits.open(self.temp("tmp.fits"), checksum=True) as h2:
...
assert "CHECKSUM" in h2[1].header
assert h2[1].header["CHECKSUM"] == "ZeAbdb8aZbAabb7a"
assert "DATASUM" in h2[1].header
assert h2[1].header["DATASUM"] == "113055149"Specifically, the last checksum and datasum should be updated to D2GXD29VD2EVD29V and 2189405276?
It seems that it is also a bug that ZHECKSUM and ZDATASUM are missing from the binary table on disk - this is also fixed in my branch but I wanted to check that we agree this is also a bug in the current code?
Any ideas on how to validate the checksums with third-party tools? (fitsverify doesn't throw an error because the actual binary table checksum is correct and I don't think it checks ZHECKSUM, which is missing anyway)
Activity
saimn commentedon May 23, 2023
I agree with you,
CHECKSUM/DATASUMfrom the image extension should have been renamed toZCHECKSUM/ZDATASUMwhen creating the compressed HDU. However I'm not sure about having both the checksum for the compressed image (ZCHECKSUM) and for the bintable (CHECKSUM). The latter seems useless. It seemsfitsiosupports both tile compression and checksums, so I will try to create a file and see what they put in it ;).saimn commentedon May 24, 2023
Hmm so fitsio just writes
CHECKSUM/DATASUM:neutrinoceros commentedon Feb 21, 2024
Quick update on this: the top example seem to have changed behavior at some point, now all checksums are identical and all datasum too. For convenience, I'll reproduce the example as a standalone script
current output (as of
astropy-6.1.dev489+ga7cd7bea56)pllim commentedon Feb 21, 2024
neutrinoceros commentedon Feb 21, 2024
I don't think so. If I understand correctly, the problem seem to have gotten worse, actually. But @astrofrog 's PR might fix it ? #15474
astrofrog commentedon Sep 3, 2024
@saimn - we should discuss the checksum question here! :)
@neutrinoceros - I don't think the example you showed is worse as such than what I showed, in the last case the checksum is the same because you put it in the header, and it did not get recomputed?
neutrinoceros commentedon Sep 4, 2024
Oh, right, I didn't realize this part was important.
Nevermind !
saimn commentedon Sep 5, 2024
Answering to #15474 (comment):
main: https://github.com/saimn/misc-astro/blob/main/astropy/Datasum.ipynbSo currently on main DATASUM is computed on the table data (i.e. compressed data), I think this is correct. And this is also what
fitsiodoes. Having DATASUM/CHECKSUM computed on the table HDU allows to verify the checksum without having to decompress the data.The problem comes from ZHECKSUM/ZDATASUM, which I think is ill-defined. The Standard just says:
So if an image HDU containing CHECKSUM/DATASUM is compressed, the keywords should be moved to ZHECKSUM/ZDATASUM. But in general we deal only with a CompImageHDU, without creating an ImageHDU before compression or after decompression. If we were to decompress to an ImageHDU then its header could contain the ZHECKSUM/ZDATASUM renamed to CHECKSUM/DATASUM. But I don't think it makes sense to do that in the header of the CompImageHDU.
This can be seen on my notebook: if the data is loaded before computed the checksum, then the file will have both ZHECKSUM/ZDATASUM and CHECKSUM/DATASUM. It seems weird that loading the data results in a different file. And in that case, when reading the file the ZHECKSUM/ZDATASUM values replace the CHECKSUM/DATASUM ones, which is confusing.
So I think we should
astrofrog commentedon Sep 6, 2024
I can see why pragmatically it probably makes sense to do as you are suggesting. Idealistically, as
CompImageHDUis a subclass ofImageHDUand is meant to represent the decompressed image HDU, the CHECKSUM and DATASUM in the header should be the ones of the decompressed image. From my personal point of view I don't think ZHECKSUM and ZDATASUM are useless - a user could always check that the checksum and datasum are correct in the decompressed data. With your approach we will need to explicitly disableCompImageHDU._update_checksumas it would otherwise compute the image one as inImageHDU.In any case, what I will do for now is make #15474 behave as it does on
main, and if/once that PR is merged, I will think some more about whether I still think we should change the behavior.astrofrog commentedon Sep 6, 2024
Looking at the code on
main, I think the intent of the code was to properly deal with ZHECKSUM and ZDATASUM. For instance, onmain, we transfer ZHECKSUM and ZDATASUM to CHECKSUM and DATASUM on read:astropy/astropy/io/fits/hdu/compressed/header.py
Lines 451 to 457 in e581eab
and we transfer them the other way on write:
astropy/astropy/io/fits/hdu/compressed/header.py
Lines 956 to 971 in e581eab
We also compute the decompressed checksum and datasum when writing out a file:
astropy/astropy/io/fits/hdu/compressed/compressed.py
Lines 866 to 883 in e581eab
and the comment even indicates that it should end up as ZHECKSUM and ZDATASUM in the file. I think there is a bug that if you don't update the data or header then the checksum might not actually be computed. This is because in this case
self._has_dataisFalseso the image HDU checksum never gets computed.In fact, it your notebook, it is actually doing the right thing on reading in the file:
In this case the checksum in the decompressed header is actually the one from ZHECKSUM and ZDATASUM!
The bottom line is that I'm certain now at this point that on
mainwe are trying to do the right thing which is:CompImageHDU.header['CHECKSUM']should be the one for the decompressed data (which is the case in thecomp-ref2.fitsexample in your notebook. This then round-trips to disk viaZHECKSUM.BinTableHDU.header['CHECKSUM']should be the compressed checksum and should not appear inCompImageHDU.headerHowever there is a bug in astropy
mainwhich is that weird things happen if you read a file and write it straight out without setting the data. The behavior is correct if you do change the data.CompImageHDUis meant to be anImageHDU(it wasn't fully on main but it does become so in my refactoring PR) so the checksum inCompImageHDUshould be as if it was anImageHDU.astrofrog commentedon Sep 6, 2024
To demonstrate the bug in
main:326be1c
if you run the tests with these changes you will get:
That is, the checksum in
CompImageHDU.headeris now the image HDU checksum! (see my examples at the top of this issue where I showedD2GXD29VD2EVD29Vwas the image checksum)astrofrog commentedon Sep 6, 2024
See #16957
saimn commentedon Sep 6, 2024
Yes, depending on whether you load the data or not you will get a different checksum in
CompImageHDU.header, that's clearly a bug and apart from that I agree that the code on main is trying to handle ZHECKSUM and ZDATASUM.Well now that we understand the current behavior, what should
CompImageHDU.header['CHECKSUM']contain is the main question.We could do as you suggest, or we could keep the checksum of the compressed data. I'm leaning towards the second option for several reasons:
CompImageHDU.header['CHECKSUM']would be empty and the table CHECKSUM which is visible in the file would be hidden ?CompImageHDU.header['CHECKSUM']is not the CHECKSUM they see in their file.astrofrog commentedon Sep 9, 2024
I see your point about computing the image checksum being expensive if the data is not already decompressed. So just to be clear, would you be happy with the following? (not mentioning DATASUM but it would behave the same as CHECKSUM)
checksum=True, the binary table checksum is written to the file on disk asCHECKSUMin the binary table header. NoZCHECKSUMis written out. When reading in that file, the table checksum is optionally verified (depending on whether requested by the user), and the decompressed header inCompImageHDU.headerwould not contain any checksum (I think it's important not to have the CHECKSUM in the decompressed header be the table checksum - better to not have it at all).ZHECKSUM, it is moved toCompImageHDU.headerasCHECKSUM, but that checksum is not validated by default.CHECKSUMin the decompressed header and get stored asZHECKSUMon disk in the binary table (we could always refuse to for lossy algorithms). We could also add a way to verify the decompressed checksum but as you said it wouldn't work for lossy algorithms.Does this sound ok? If so, I'll update my refactoring PR to follow this behavior.
saimn commentedon Sep 10, 2024
Ok sounds good, but if we do 2) I think we have to do 3), i.e. if CHECKSUM is present in
CompImageHDU.headerit must be written as ZHECKSUM. It's already done on main, so we should keep that. And users already have a way to have the image CHECKSUM, which is to create a CompImageHDU from an ImageHDU containing the checksum, which is consistent with the Standard definition of ZHECKSUM.