Skip to content

Pyfits: Incorrect writing of non contiguous data in compressed FITS #2150

Description

@pchanial

Using astropy 0.3.dev4315, the following code prints False instead of True. If ImageHDU is substituted to CompImageHDU, it works correctly and prints True.

orig = np.arange(100, dtype=float).reshape((10, 10), order='f')
primary = fits.PrimaryHDU()
hdu = fits.CompImageHDU(orig)
hdulist = fits.HDUList([primary, hdu])
hdulist.writeto('test.fits', clobber=True)
actual = fits.open('test.fits')[1].data
print np.all(orig == actual)

Activity

  1. embray commented on Feb 27, 2014

    @embray
    Member

    That makes sense--CFITSIO is going to assume the data is stored in C-order and operate on it as such. Since there's no way (I didn't think) to change how CFITSIO is going to work on the data it would have to be transposed before going through the compression routines. This of course introduces some extra overhead but is not insurmountable. Still, better to just use C-order in the first place if possible.

  2. pchanial commented on Feb 27, 2014

    @pchanial
    Author

    Well, the user should not have to worry about his data being strided or not when passing them to pyfits. The ImageHDU class works flawlessly in that regard. You might consider passing np.ascontiguousarray(the_input_image) to CFITSIO. It will make a copy only if the data is not C contiguous.

  3. embray commented on Feb 27, 2014

    @embray
    Member

    Oh I agree completely. I was just wondering if in the meantime any trouble caused by this could be avoided.

    I wonder whether it would be better to make a copy or to just transpose the array in-place. It's a tradeoff...

  4. astrofrog commented on Feb 27, 2014

    @astrofrog
    Member

    Modifying the data in-place seems like an undesirable side-effect if the user needs the data in fortran-order, no? It seems that a copy would make sense, maybe with a warning that this is inefficient?

  5. pchanial commented on Feb 27, 2014

    @pchanial
    Author

    The issue is more general than that: a non C-contiguous array is not necessarily F-contiguous. Writing a[::2] would fail too I'm pretty sure. Pyfits is already quite verbose, I don't think a warning would be good. There are hundreds of places in scipy where the data is made contiguous in some ordering, without warnings. Pyfits/CFITSIO behaviour should be documented though. And ImageHDU doesn't issue a warning. Doesn't it make a copy for non C-contiguous arrays ?

  6. added this to the v0.3.2 milestone on Feb 27, 2014
  7. embray commented on Feb 28, 2014

    @embray
    Member

    No. In the example you gave they're really two very different arrays:

    >>> a = np.arange(100, dtype='uint8').reshape((10, 10))
    >>> a
    array([[ 0,  1,  2,  3,  4,  5,  6,  7,  8,  9],
           [10, 11, 12, 13, 14, 15, 16, 17, 18, 19],
           [20, 21, 22, 23, 24, 25, 26, 27, 28, 29],
           [30, 31, 32, 33, 34, 35, 36, 37, 38, 39],
           [40, 41, 42, 43, 44, 45, 46, 47, 48, 49],
           [50, 51, 52, 53, 54, 55, 56, 57, 58, 59],
           [60, 61, 62, 63, 64, 65, 66, 67, 68, 69],
           [70, 71, 72, 73, 74, 75, 76, 77, 78, 79],
           [80, 81, 82, 83, 84, 85, 86, 87, 88, 89],
           [90, 91, 92, 93, 94, 95, 96, 97, 98, 99]], dtype=uint8)
    >>> a.tostring()
    '\x00\x01\x02\x03\x04\x05\x06\x07\x08\t\n\x0b\x0c\r\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f !"#$%&\'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abc'
    >>> b = np.arange(100, dtype='uint8').reshape((10, 10), order='F')
    >>> b
    array([[ 0, 10, 20, 30, 40, 50, 60, 70, 80, 90],
           [ 1, 11, 21, 31, 41, 51, 61, 71, 81, 91],
           [ 2, 12, 22, 32, 42, 52, 62, 72, 82, 92],
           [ 3, 13, 23, 33, 43, 53, 63, 73, 83, 93],
           [ 4, 14, 24, 34, 44, 54, 64, 74, 84, 94],
           [ 5, 15, 25, 35, 45, 55, 65, 75, 85, 95],
           [ 6, 16, 26, 36, 46, 56, 66, 76, 86, 96],
           [ 7, 17, 27, 37, 47, 57, 67, 77, 87, 97],
           [ 8, 18, 28, 38, 48, 58, 68, 78, 88, 98],
           [ 9, 19, 29, 39, 49, 59, 69, 79, 89, 99]], dtype=uint8)
    >>> b.tostring()
    '\x00\n\x14\x1e(2<FPZ\x01\x0b\x15\x1f)3=GQ[\x02\x0c\x16 *4>HR\\\x03\r\x17!+5?IS]\x04\x0e\x18",6@JT^\x05\x0f\x19#-7AKU_\x06\x10\x1a$.8BLV`\x07\x11\x1b%/9CMWa\x08\x12\x1c&0:DNXb\t\x13\x1d\'1;EOYc'
    

    At a certain level PyFITS doesn't care what the shape or index order of the array is. It's just a raw sequence of bytes. This is where things run into trouble with the compression code in particular, because once you've written to the file and read it back again it has no way of knowing that the array that was passed in was "Fortran order". That information is not in any way preserved.

  8. embray commented on Feb 28, 2014

    @embray
    Member

    @astrofrog In place transposition of the data would only happen before writing, and then restored after. We already do this with regard to byte order. Unfortunately transposing a large array is a lot more expensive that byte swapping (which is about as fast as it can be on modern architectures).

  9. pchanial commented on Feb 28, 2014

    @pchanial
    Author

    @embray not sure what you're answering no, but the fix you're proposing is not general enough. As I anticipated:

    >>> orig = np.arange(10, dtype=float)[::2]
    >>> primary = fits.PrimaryHDU()
    >>> hdu = fits.CompImageHDU(orig)
    >>> hdulist = fits.HDUList([primary, hdu])
    >>> hdulist.writeto('test_strided.fits', clobber=True)
    >>> print 'orig:', orig
    orig: [ 0.  2.  4.  6.  8.]
    >>> print 'actual:', fits.open('test_strided.fits')[1].data
    actual: [ 0.  1.  2.  3.  4.]
    

    What matters is whether or not the data is contiguous in memory or not.

  10. embray commented on Feb 28, 2014

    @embray
    Member

    Ah I see, good point. In that case it would be absolutely necessary to copy the data into a contiguous buffer. No way around it there :/ At least in the C-contiguous vs. F-contiguous case the issue could be fixed without copying by transposing the data in-place. But that wouldn't work when it comes to arbitrary strides (well, in principle they could be handled by shuffling data around in-place as well, but at that point a copy is much simpler).

  11. self-assigned this
    on Apr 18, 2014
  12. modified the milestones: , v0.3.2 on Apr 30, 2014
  13. 4 remaining items

  14. modified the milestones: v0.4.2, v0.4.3 on Sep 23, 2014
  15. modified the milestone: v0.4.3 on Nov 21, 2014
  16. cdeil commented on Mar 22, 2018

    @cdeil
    Member

    We ran into this issue with Gammapy, see here:
    gammapy/gammapy#1346

    [updated test case and output]

    Minimal test case to reproduce:

    import numpy as np
    from astropy.io import fits
    
    data = np.arange(6).reshape((3, 2)).T
    
    hdu = fits.PrimaryHDU(data)
    filename = 'tmp.fits.gz'
    hdu.writeto(filename, overwrite=True)
    hdu2 = fits.open(filename)[0]
    
    data2 = hdu2.data
    
    print('data:\n', data)
    print('data2:\n', data2)
    

    gives the incorrect result (i.e. data is different after write / read):

    data:
     [[0 2 4]
     [1 3 5]]
    data2:
     [[0 1 2]
     [3 4 5]]
    

    When removing the .gz, i.e. writing just to tmp.fits, the data comes back OK.

    This is very confusing for users, either an error, or making it work via a copy would be preferable.

  17. MSeifert04 commented on Mar 22, 2018

    @MSeifert04
    Contributor

    I think I'm blind. I can't see any difference between those two. Could you clarify?

  18. cdeil commented on Mar 22, 2018

    @cdeil
    Member

    @MSeifert04 - I had copy & pasted the wrong test case and output. I've updated my last comment just now, please refresh.

  19. MSeifert04 commented on Mar 22, 2018

    @MSeifert04
    Contributor

    Ah thanks!

    That's not good ... I will look into this (although probably not this week) but if it's indeed effort-high (label) raising an exception might make sense. :)

  20. cdeil commented on Mar 22, 2018

    @cdeil
    Member

    You might consider passing np.ascontiguousarray(the_input_image) to CFITSIO. It will make a copy only if the data is not C contiguous.

    @pchanial - Would that still be your proposed change to astropy.io.fits to handle this issue?

  21. saimn commented on Mar 22, 2018

    @saimn
    Contributor

    Reading the discussion above, np.ascontiguousarray seems the easiest way to go for now, not the most efficient but at least it would fix this usecase.

  22. added 2 commits that reference this issue on Feb 19, 2020
    c4842c0
    c1fc559
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions