Skip to content

astropy.io.fits: Unexpected behavior of scaled data in random groups and binary table #2751

Open

Description

@piyanatk

When you create a GroupData object and set PZEROn keywords to non-zero values, once you save out the GroupsHDU object to a file and reread it, the PZEROn values replace all the values inside parameter arrays instead of properly scaling the array. Data type of the arrays will also not be preserved and change to float64.

>>> from astropy.io import fits
>>> import numpy as np
>>> imdata = np.zeros(20, dtype=np.float32)
>>> imdata.shape = (5, 1, 1, 2, 2)
>>> pdata = np.arange(5, dtype=np.float32) * 10
>>> x = fits.GroupData(imdata, parnames=['PAR1',], pardata=[pdata,],
...                    parbzeros=[3.14,],
...                    parbscales=[1.,], bitpix=-32)
>>> hdu = fits.GroupsHDU(x)
>>> hdu.writeto('test_grouptb_scaled.fits', clobber=True)
>>> t = fits.open('test_grouptb_scaled.fits')
>>> print hdu.data['par1']
[  0.  10.  20.  30.  40.]
>>> print hdu.data['par1'].dtype
float32
>>> print t[0].data['par1']
[  3.14000000e+00   3.14000000e+00   3.14000000e+00  -1.58456325e+29
   3.14000000e+00]
>>> print t[0].data['par1'].dtype
float64
>>> hdu.header['pzero1']
3.14
>>> t[0].header['pzero1']
3.14

Scaling at read-in for a random group data seems to work properly if the file was written with other software packages (for example VLA data converted to AIPS UVFITS), but the data type of the array after scaling will always be float64 - not preserved - and you still run into the above issue if you save out the HDU.

The binary table has somewhat similar problems. When you create a binary table hdu, array values in a column with non-default TZEROn keyword will be shown as if they have been replaced by negative of the TZEROn value. But when you save out the HDU and reread it, the array values seem to not have been replaced although the data type is, again, not preserved, and there are some accuracy problems.

>>> names = np.array(['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j'])
>>> values = np.arange(10, dtype=np.float32)
>>> c1 = fits.Column(name='names', format='1A', array=names)
>>> c2 = fits.Column(name='values', format='E', bzero=24251.3141, array=values)
>>> tbhdu = fits.BinTableHDU.from_columns([c1, c2])
>>> print repr(tbhdu.header)
XTENSION= 'BINTABLE'           / binary table extension                         
BITPIX  =                    8 / array data type                                
NAXIS   =                    2 / number of array dimensions                     
NAXIS1  =                    5 / length of dimension 1                          
NAXIS2  =                   10 / length of dimension 2                          
PCOUNT  =                    0 / number of group parameters                     
GCOUNT  =                    1 / number of groups                               
TFIELDS =                    2 / number of table fields                         
TTYPE1  = 'names   '                                                            
TFORM1  = '1A      '                                                            
TTYPE2  = 'values  '                                                            
TFORM2  = 'E       '                                                            
TZERO2  =           24251.3141                                                  
>>> tbhdu.writeto('test_bintb.fits', clobber=True)
>>> test_tb = fits.open('test_bintb.fits')
>>> values
array([ 0.,  1.,  2.,  3.,  4.,  5.,  6.,  7.,  8.,  9.], dtype=float32)
>>> tbhdu.data['values']
array([-24251.31445312, -24250.31445312, -24249.31445312, -24248.31445312,
       -24247.31445312, -24246.31445312, -24245.31445312, -24244.31445312,
       -24243.31445312, -24242.31445312], dtype=float32)
>>> test_tb[1].data['values']
array([ -3.53125000e-04,   9.99646875e-01,   1.99964687e+00,
         2.99964687e+00,   3.99964687e+00,   4.99964687e+00,
         5.99964687e+00,   6.99964687e+00,   7.99964687e+00,
         8.99964687e+00])
>>> test_tb[1].data['values'].dtype
dtype('float64')

Both of the above issues seem be related to how astropy handle scaled data in a binary table, and my suggestion for a solution is to not scale a binary table data at read-in, or at least provide an option for that like in the IDL Astro library.

Activity

  1. piyanatk commented on Jul 15, 2014

    @piyanatk
    Author

    Okay. So the conversion from 32-bit float to 64-bit float is expected. I have tried do_not_scale_image_data=True, but it does not work with table data and random group parameter arrays. I will try mode='update' and scale_back=True and get back to you.

    However, the behavior here is not just about conversion from 32-bit float to 64-bit float. The scaling just does not work as expected from the formula:

    physical_value = BZERO + BSCALE * array_value
    
  2. piyanatk commented on Jul 16, 2014

    @piyanatk
    Author

    Update: open a random group structure with mode='update' andscale_back=True`.

    In [1]: import astropy.io.fits as fits
    In [2]: t1 = fits.open('test1.fits', mode='update', scale_back=True)
    In [3]: t1[0].header
    Out[3]: 
    SIMPLE  =                    T / conforms to FITS standard                      
    BITPIX  =                  -32 / array data type                                
    NAXIS   =                    5 / number of array dimensions                     
    NAXIS1  =                    0                                                  
    NAXIS2  =                    2                                                  
    NAXIS3  =                    2                                                  
    NAXIS4  =                    1                                                  
    NAXIS5  =                    1                                                  
    EXTEND  =                    T                                                  
    GROUPS  =                    T / has groups                                     
    PCOUNT  =                    1 / number of parameters                           
    GCOUNT  =                    5 / number of groups                               
    PTYPE1  = 'PAR1    '                                                            
    PZERO1  =                 3.14                                                  
    In [4]: t1.info
    Out[4]: <bound method HDUList.info of [<astropy.io.fits.hdu.groups.GroupsHDU object at 0x1091b2a10>]>
    In [5]: t1.info()
    Filename: test1.fits
    No.    Name         Type      Cards   Dimensions   Format
    0    PRIMARY     GroupsHDU       14   (2, 2, 1, 1)   float32   5 Groups  1 Parameters
    In [6]: t1[0].data
    Out[6]: 
    GroupData([ (3.1400000000000001, array([[[[ 0.,  0.],
             [ 0.,  0.]]]], dtype=float32)),
           (3.1400000000000001, array([[[[ 0.,  0.],
             [ 0.,  0.]]]], dtype=float32)),
           (3.1400000000000001, array([[[[ 0.,  0.],
             [ 0.,  0.]]]], dtype=float32)),
           (5.1400000000000006, array([[[[ 0.,  0.],
             [ 0.,  0.]]]], dtype=float32)),
           (3.1400000000000001, array([[[[ 0.,  0.],
             [ 0.,  0.]]]], dtype=float32))], 
          dtype=[('PAR1', '>f4'), ('DATA', '>f4', (1, 1, 2, 2))])
    In [7]: t1[0].data['par1']
    Out[7]: array([ 3.14,  3.14,  3.14,  5.14,  3.14])
    In [8]: t1[0].data['par1'] = np.arange(5)
    In [9]: t1[0].data['par1']
    Out[9]: array([ 0.,  1.,  2.,  3.,  4.])
    In [10]: t1.flush()
    In [11]: t1.close()
    In [12]: del(t1)
    In [13]: t1 = fits.open('test1.fits')
    In [14]: t1[0].data['par1']
    Out[14]: array([ 3.14,  3.14,  3.14,  5.14,  3.14])
    # Expect array([0., 1., 2., 3., 4.])
  3. piyanatk commented on Jul 16, 2014

    @piyanatk
    Author

    @embray I think you are right that scaling at read-in is not an issue here. I did more test with the random groups structure, and I think the problem in the first post happens when you write out a group hdu with non-default scaling parameters to a fits file. Please the code snippet below, and sorry for a long post here.

    >>> from astropy.io import fits
    >>> import numpy as np
    >>> imdata = np.zeros(20, dtype=np.float32)
    >>> imdata.shape = (5, 1, 1, 2, 2)
    >>> pdata = np.arange(5, dtype=np.float32)
    
    # Create a group data and group hdu with scaling, writing out to test1.fits.
    >>> x1 = fits.GroupData(imdata, parnames=['PAR1',], pardata=[pdata,],
                                            parbzeros=[3.14,], parbscales=[1.,], bitpix=-32)
    >>> hdu1 = fits.GroupsHDU(x1)
    >>> hdu1.header['pzero1']
    3.14
    >>> hdu1.data['par1']
    array([ 0.,  1.,  2.,  3.,  4.], dtype=float32)
    >>> hdu1.writeto('test1.fits', clobber=True)
    
    # Same as above with the same data but no scaling, writing out to test2.fits.
    >>> x2 = fits.GroupData(imdata, parnames=['PAR1',], pardata=[pdata,], bitpix=-32)
    >>> hdu2 = fits.GroupsHDU(x2)
    >>> hdu2.header['pzero1']
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
      File "/Users/piyanat/src/anaconda/envs/conda-sim/lib/python2.7/site-packages/astropy/io/fits/header.py", line 153, in __getitem__
        card = self._cards[self._cardindex(key)]
      File "/Users/piyanat/src/anaconda/envs/conda-sim/lib/python2.7/site-packages/astropy/io/fits/header.py", line 1725, in _cardindex
        raise KeyError("Keyword %r not found." % keyword)
    KeyError: "Keyword 'PZERO1' not found."
    >>> hdu2.data['par1']
    array([ 0.,  1.,  2.,  3.,  4.], dtype=float32)
    >>> hdu2.writeto('test2.fits', clobber=True)
    
    # test1.fits data has scaling applied when touched, but data['par1'] is replaced by PZERO1 value.
    >>> t1 = fits.open('test1.fits')                                           
    >>> t1[0].data['par1']
    array([ 3.14,  3.14,  3.14,  3.14,  3.14])
    
    # test2.fits is okay.
    >>> t2 = fits.open('test2.fits')                                                   
    >>> t2[0].data['par1']
    array([ 0.,  1.,  2.,  3.,  4.], dtype=float32)
    
    # Now we change PZERO1 keyword in test1.fits header before the data is touched.
    >>> t1.close()
    >>> del(t1)
    >>> t1 = fits.open('test1.fits')
    >>> t1[0].header['pzero1'] = 0.
    >>> t1[0].data['par1']
    array([  0.00000000e+00,   1.40129846e-45,   0.00000000e+00,
             1.40129846e-45,   0.00000000e+00], dtype=float32)
  4. embray commented on Jul 16, 2014

    @embray
    Member

    Yeah, I wouldn't be surprised if this doesn't work quite right for random groups HDUs. It's not a use case cross section that I think is tested much. Reworking the UI for this is high on the todo list (though automatic rescaling will remain the default).

    Thanks for investigating this though. I'll see what I can do about this when I get a chance.

  5. self-assigned this
    on Jun 9, 2015
  6. cmarmo commented on Mar 29, 2026

    @cmarmo
    Member

    Dear maintainers,
    I have rerun the code above with astropy '8.0.0.dev546+gd8705bd60', after adapting some parameters (clobber -> overwrite).
    The issue is still there.

    Python 3.14.3 (main, Feb  4 2026, 00:00:00) [GCC 15.2.1 20260123 (Red Hat 15.2.1-7)] on linux
    Type "help", "copyright", "credits" or "license" for more information.
    >>> from astropy.io import fits
    >>> import numpy as np
    >>> imdata = np.zeros(20, dtype=np.float32)
    >>> imdata.shape = (5, 1, 1, 2, 2)
    >>> pdata = np.arange(5, dtype=np.float32)
    >>> x1 = fits.GroupData(imdata, parnames=['PAR1',], pardata=[pdata,],
    ...                                         parbzeros=[3.14,], parbscales=[1.,], bitpix=-32)
    >>> hdu1 = fits.GroupsHDU(x1)
    >>> hdu1.header['pzero1']
    3.14
    >>> hdu1.data['par1']
    array([0., 1., 2., 3., 4.], dtype=float32)
    >>> hdu1.writeto('test1.fits', overwrite=True)
    >>> x2 = fits.GroupData(imdata, parnames=['PAR1',], pardata=[pdata,], bitpix=-32)
    >>> hdu2 = fits.GroupsHDU(x2)
    >>> hdu2.header['pzero1']
    Traceback (most recent call last):
      File "<python-input-13>", line 1, in <module>
        hdu2.header['pzero1']
        ~~~~~~~~~~~^^^^^^^^^^
      File "/home/cmarmo/software/astropy/astropy/io/fits/header.py", line 170, in __getitem__
        card = self._cards[self._cardindex(key)]
                           ~~~~~~~~~~~~~~~^^^^^
      File "/home/cmarmo/software/astropy/astropy/io/fits/header.py", line 1719, in _cardindex
        raise KeyError(f"Keyword {keyword!r} not found.")
    KeyError: "Keyword 'PZERO1' not found."
    >>> hdu2.data['par1']
    array([0., 1., 2., 3., 4.], dtype=float32)
    >>> hdu2.writeto('test2.fits', overwrite=True)
    >>> t1 = fits.open('test1.fits')      
    >>> t1[0].data['par1']
    array([3.14, 3.14, 3.14, 3.14, 3.14])
    >>> t2 = fits.open('test2.fits')     
    >>> t2[0].data['par1']
    array([0., 1., 2., 3., 4.], dtype='>f4')
    >>> t1.close()
    >>> del(t1)
    >>> t1 = fits.open('test1.fits')
    >>> t1[0].header['pzero1'] = 0.
    >>> t1[0].data['par1']
    array([-1.765449e-09,  1.387285e-43,  1.513402e-43,  4.484155e-44,
            1.387285e-43], dtype='>f4')
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