Skip to content

CompImageHDU: a header-only edit in mode="update" silently rescales the data of a scaled (BSCALE/BZERO) compressed image, cumulatively #20368

Description

@mcomelli

Description

Opening a FITS file that contains a compressed image HDU carrying BSCALE/BZERO with fits.open(..., mode="update"), and modifying only the header — without ever accessing .data — rescales the stored data. There is no exception, no warning, and the process exits with status 0. The corruption is cumulative: every further header-only update applies the scale factor again, and with a non-zero BZERO the values can change sign.

The trigger is precisely that the data are not loaded. If .data has been accessed before the header is modified, the file is written correctly. This is why the defect survives interactive use and strikes scripts, where nobody reads an array they do not need. In our case a script that added one keyword to the header of 464 compressed frames halved the data of all of them; it was found by an invariance check on a downstream quality indicator, not by any error.

Cause. It is an ordering problem in CompImageHDU._prewriteto (astropy/io/fits/hdu/compressed/compressed.py). Accessing .data returns the array already scaled and, as a side effect, strips BSCALE/BZERO from the image header — that bookkeeping is correct. But _get_bintable_without_data() snapshots the image header into the binary-table header before _add_data_to_bintable() triggers that access, so the table header keeps a BSCALE that the reader applies a second time on the next open. The method is unchanged on main at the time of writing.

Related. #15255 (BSCALE disappears when copying ImageHDUs) concerns the uncompressed ImageHDU, drops keywords and leaves the values intact; here the class is CompImageHDU, the keywords survive and the values change.

Workaround on an unpatched installation: touch hdu.data before modifying the header, or open with do_not_scale_image_data=True.

A fix and a regression test exist and can be submitted as a pull request. The fix materialises the data before the header is copied and restores the integer representation the file was written with. A first version that only accessed .data preserved the values but turned the on-disk int16 + BSCALE into float32 (+133% file size); the adopted version keeps the file unchanged after repeated updates. The proposed regression test (7 parametrised cases) fails 3/7 without the fix and passes 7/7 with it. Astropy's own tests in io/fits/hdu/compressed/, test_image.py, test_uint.py and test_core.py — 723 passed, 6 skipped, 100 xfailed — give the identical result with and without the fix, i.e. no existing test covers this case.

Expected behavior

A header-only update in mode="update" must leave the stored data unchanged and must keep BSCALE/BZERO (and the integer representation) exactly as written. Reading the file back after any number of header edits must return the same values as before them.

How to Reproduce

  1. Write a compressed image HDU with integer data and BSCALE = 0.5.
  2. Open it in mode="update" and add one header keyword, without touching .data. Repeat.
  3. Read the data back after each update.
import numpy as np
from astropy.io import fits

path = "scaled.fits"
hdu = fits.CompImageHDU(np.arange(1, 101, dtype=np.int16).reshape(10, 10))
hdu.header["BSCALE"] = 0.5
fits.HDUList([fits.PrimaryHDU(), hdu]).writeto(path, overwrite=True)

print(fits.getdata(path).sum())              # 2525.0 = 0.5 * sum(1..100), the true value
for i in range(3):
    with fits.open(path, mode="update") as hdul:
        hdul[1].header["COMMENT"] = f"header-only update {i}"   # no access to .data
    print(fits.getdata(path).sum())

Output on astropy 8.0.1:

2525.0
1250.0
612.5
294.0

The same loop with _ = hdul[1].data inserted before the header edit prints 2525.0 four times.

Versions

platform
--------
platform.platform() = 'Windows-10-10.0.19045-SP0'
platform.version() = '10.0.19045'
platform.python_version() = '3.14.6'

packages
--------
astropy              8.0.1
numpy                2.5.2
scipy                1.18.0
matplotlib           3.11.1
pandas               3.0.5
pyerfa               2.0.1.5

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions