Skip to content

fits.Z cannot be opened using automatic decompression #10714

Closed

Description

@astrom-tom

Description

When trying to open a fits.Z (ESO download format) the file does can be read (link to file https://drive.google.com/file/d/1PoQhABH90aQwwgUSmzRqrj-yPvGWLvXW/view?usp=sharing )

Expected behavior

I expect the header to be read normally. Important to note that it works with the fitsio library.

Actual behavior

---------------------------------------------------------------------------
UnicodeDecodeError                        Traceback (most recent call last)
~/.local/lib/python3.8/site-packages/astropy/io/fits/hdu/base.py in _readfrom_internal(cls, data, header, checksum, ignore_missing_end, **kwargs)
    405                     # that are needed to build the HDU object.
--> 406                     header_str, header = _BasicHeader.fromfile(data)
    407                 except Exception:

~/.local/lib/python3.8/site-packages/astropy/io/fits/header.py in fromfile(cls, fileobj)
   2076         try:
-> 2077             header_str, cards = parse_header(fileobj)
   2078             _check_padding(header_str, BLOCK_SIZE, False)

astropy/io/fits/_utils.pyx in astropy.io.fits._utils.parse_header()

UnicodeDecodeError: 'ascii' codec can't decode byte 0x9d in position 1: ordinal not in range(128)

During handling of the above exception, another exception occurred:

OSError                                   Traceback (most recent call last)
<ipython-input-2-0fb814c35786> in <module>
----> 1 fits.getheader('VISIR.2017-01-05T09:04:11.258.fits.Z')

~/.local/lib/python3.8/site-packages/astropy/io/fits/convenience.py in getheader(filename, *args, **kwargs)
    103 
    104     mode, closed = _get_file_mode(filename)
--> 105     hdulist, extidx = _getext(filename, mode, *args, **kwargs)
    106     try:
    107         hdu = hdulist[extidx]

~/.local/lib/python3.8/site-packages/astropy/io/fits/convenience.py in _getext(filename, mode, ext, extname, extver, *args, **kwargs)
   1047         raise TypeError('extver alone cannot specify an extension.')
   1048 
-> 1049     hdulist = fitsopen(filename, mode=mode, **kwargs)
   1050 
   1051     return hdulist, ext

~/.local/lib/python3.8/site-packages/astropy/io/fits/hdu/hdulist.py in fitsopen(name, mode, memmap, save_backup, cache, lazy_load_hdus, **kwargs)
    162         raise ValueError(f'Empty filename: {name!r}')
    163 
--> 164     return HDUList.fromfile(name, mode, memmap, save_backup, cache,
    165                             lazy_load_hdus, **kwargs)
    166 

~/.local/lib/python3.8/site-packages/astropy/io/fits/hdu/hdulist.py in fromfile(cls, fileobj, mode, memmap, save_backup, cache, lazy_load_hdus, **kwargs)
    401         """
    402 
--> 403         return cls._readfrom(fileobj=fileobj, mode=mode, memmap=memmap,
    404                              save_backup=save_backup, cache=cache,
    405                              lazy_load_hdus=lazy_load_hdus, **kwargs)

~/.local/lib/python3.8/site-packages/astropy/io/fits/hdu/hdulist.py in _readfrom(cls, fileobj, data, mode, memmap, save_backup, cache, lazy_load_hdus, **kwargs)
   1077 
   1078         # Make sure at least the PRIMARY HDU can be read
-> 1079         read_one = hdulist._read_next_hdu()
   1080 
   1081         # If we're trying to read only and no header units were found,

~/.local/lib/python3.8/site-packages/astropy/io/fits/hdu/hdulist.py in _read_next_hdu(self)
   1152                                 fileobj.seek(offset, os.SEEK_SET)
   1153 
-> 1154                         hdu = _BaseHDU.readfrom(fileobj, **kwargs)
   1155                     except EOFError:
   1156                         self._read_all = True

~/.local/lib/python3.8/site-packages/astropy/io/fits/hdu/base.py in readfrom(cls, fileobj, checksum, ignore_missing_end, **kwargs)
    323             fileobj = _File(fileobj)
    324 
--> 325         hdu = cls._readfrom_internal(fileobj, checksum=checksum,
    326                                      ignore_missing_end=ignore_missing_end,
    327                                      **kwargs)

~/.local/lib/python3.8/site-packages/astropy/io/fits/hdu/base.py in _readfrom_internal(cls, data, header, checksum, ignore_missing_end, **kwargs)
    411                     # in the wild.
    412                     data.seek(header_offset)
--> 413                     header = Header.fromfile(data,
    414                                              endcard=not ignore_missing_end)
    415             hdu_fileobj = data

~/.local/lib/python3.8/site-packages/astropy/io/fits/header.py in fromfile(cls, fileobj, sep, endcard, padding)
    511                         break
    512 
--> 513             return cls._from_blocks(block_iter, is_binary, sep, endcard,
    514                                     padding)[1]
    515         finally:

~/.local/lib/python3.8/site-packages/astropy/io/fits/header.py in _from_blocks(cls, block_iter, is_binary, sep, endcard, padding)
    594             # TODO: Pass this error to validation framework as an ERROR,
    595             # rather than raising an exception
--> 596             raise OSError('Header missing END card.')
    597 
    598         header_str = ''.join(read_blocks)

OSError: Header missing END card.

Steps to Reproduce

from astropy.io import fits
fits.getheader('VISIR.2017-01-05T09:04:11.258.fits.Z')

System Details

import platform; print(platform.platform())
-->Linux-5.4.60-1-lts-x86_64-with-glibc2.2.5
import sys; print("Python", sys.version) 

-->Python 3.8.5 (default, Jul 27 2020, 08:42:51) 
[GCC 10.1.0]

import numpy; print("Numpy", numpy.__version__)
-->Numpy 1.17.4

import astropy; print("astropy", astropy.__version__)
-->astropy 4.0.1.post1

import scipy; print("Scipy", scipy.__version__)
Scipy 1.3.3

import matplotlib; print("Matplotlib", matplotlib.__version__)
Matplotlib 3.3.0

Activity

  1. saimn commented on Sep 7, 2020

    @saimn
    Contributor

    Salut Romain,

    The problem is that there is no way to decompress .Z files with the stdlib. I found two packages to read those:
    https://github.com/ionelmc/python-unlzw
    https://github.com/umeat/unlzw
    I guess we could have an optional dependency on one of these. The two are based on https://mathematica.stackexchange.com/questions/60531/how-can-i-read-compressed-z-file-automatically-by-mathematica/60879#60879, the first one wrapping the C code, and the second reimplementing in Python.

  2. astrom-tom commented on Sep 7, 2020

    @astrom-tom
    Author

    Salut Simon!

    Thanks for your message! I created this https://github.com/astrom-tom/dfitspy package that I recently migrated to astropy (was using fitsio before) but I lost the possibility to read .Z files at the same time...
    That would be awesome if it was possible to add the support to it, it is the format that come from the ESO archive so I am sure it would be useful for a lot o people.

  3. astrom-tom commented on Sep 7, 2020

    @astrom-tom
    Author

    Sorry, hit the wrong button

  4. saimn commented on Sep 7, 2020

    @saimn
    Contributor

    Supporting this compression format with one of the libraries above (probably the one wrapping the C code, for performance) would be easy, but - at first view - a drawback is that the complete file would have to be decompressed. Which is not optimal to read headers... I'm not sure if there is a better alternative.
    Btw, fitsheader from Astropy has a fitsort-like mode, but i guess diftspy's goal is to have the same output as dfits.

  5. dhomeier commented on Sep 9, 2020

    @dhomeier
    Contributor

    Apart from the in-memory reading that seems doable (also as a temporary workaround, if you're interested):

    from astropy.io import fits
    from unlzw import unlzw
    hdr = fits.HDUList.fromstring(unlzw(open('VISIR.2017-01-05T09_04_11.258.fits.Z', mode='rb').read()))[0].header
    hdr.get('ORIGFILE')
    
  6. m-samland commented on Jan 28, 2021

    @m-samland

    Is there any progress on this issue? Considering that the whole ESO archive is using fits.Z it seems quite important.

    Does anyone have a good temporary workaround for reading in the data instead of the header? The above snipped works for headers, but throws a "TypeError: buffer is too small for requested array" when you try to access the data instead of the header.

  7. saimn commented on Jan 28, 2021

    @saimn
    Contributor

    It's not the whole ESO archive. My guess would be that it concerns only a few instruments, @astrom-tom probably knows better about this ?
    And if it was so important, I guess ESO could contribute some resources to support this format ;). If you are a user, one thing to do would be to contact their support (or have a PI do it) asking them to help doing this.
    As mentioned above there are 2 Python libraries supporting this format, but both have not seen a commit since 2017/2018 so to have a good support in Astropy it requires some work. Testing those libraries, checking the performance of the pure Python one versus the C one. Adding support in Astropy, and unit tests.
    And the authors of those libraries may not be willing to support it on the long term, so what do we do if there is a problem with the one we use ? (The error you get, "TypeError: buffer is too small for requested array", may be a good example. Which one did you try ?)

  8. dhomeier commented on Jan 28, 2021

    @dhomeier
    Contributor

    I could reproduce the buffer size exception with the compiled library python-unlzw (which is the one that gets installed by pip install unlzw, to add more confusion). This could indicate further trouble with correctly building the underlying C library (or using a precompiled one from a different machine).

    The pure Python version, which is effectively a single file https://raw.githubusercontent.com/umeat/unlzw/master/unlzw/__init__.py
    works for me with the data as well, but as expected is quite resource-intensive (O(1 s / MB) decompression time, and it seems up to 10 x the raw data size in temporary memory usage).

    As a feasible workaround for now I would rather recommend to use command-line uncompress on the whole bunch of files (and re-compress with bzip2 or gzip if really necessary – for the example file the .Z version offered < 5 % savings in file size).

  9. dhomeier commented on Jan 28, 2021

    @dhomeier
    Contributor

    And the authors of those libraries may not be willing to support it on the long term, so what do we do if there is a problem with the one we use ? (The error you get, "TypeError: buffer is too small for requested array", may be a good example. Which one did you try ?)

    It might be a bug or a Python 3 problem, but

    https://github.com/ionelmc/python-unlzw/blob/a5f6774d33e862ca5504666326efbc1579f6a4ed/src/unlzw/__init__.py#L15

    did not work because ffi.string() stops at the first null character, which you will inevitably encounter at some byte of a FITS data unit. Changing this to

    return _ffi.buffer(out[0], outlen[0])[:]
    

    lets it succeed with performance on par or better than the bz2 or lzma decompressors.

  10. changed the title [-]fits.Z cannot be open[/-] [+]fits.Z cannot be opened using automatic decompression[/+] on Jan 28, 2021
  11. mfeldt commented on Apr 16, 2021

    @mfeldt

    It's not the whole ESO archive. My guess would be that it concerns only a few instruments, @astrom-tom probably knows better about this ?
    And if it was so important, I guess ESO could contribute some resources to support this format ;). If you are a user, one thing to do would be to contact their support (or have a PI do it) asking them to help doing this.

    To my knowledge it is at least a very large part of the ESO archive. Moreover reading and writing .Z compression is pretty standard and also supported by cfitsio, so I guess they chose such a standard format in order to not have to devote resources to support reading and writing.

  12. self-assigned this
    on May 2, 2021
  13. kYwzor commented on Feb 17, 2025

    @kYwzor
    Member

    Just an heads-up, I'm currently working on a solution for this. My goal is to have an implementation that can read any .Z LZW file transparently i.e. it doesn't require decompressing the entire file and holding everything in memory nor creating a temporary file (similar to existing gzip/bz2 packages). The big win here is that it would enable very fast reads of the primary headers.

    I already have a working prototype written in pure Python which I think is quite optimized. In some very early testing, for full file decompression, my implementation seems to always be faster than the other existing pure Python implementations; in small files (tens of MBs) it's up to ~2x faster, though the margin seems to shrink in larger files (hundreds of MBs). If are working with very large FITS files and we're only interested in reading the header, my implementation can easily achieve reads on a millisecond scale for most FITS files, which logically makes it thousands of times faster than the alternatives (which can only do full decompression).

    I plan on publishing this as a package and making a PR integrating it into Astropy before the end of March. I'm currently working on cleaning up the interfaces, validating the implementation and doing a more in-depth performance characterization. If anyone wants to help me with this, I'd quite appreciate it if you could send me or point me to .Z files that you know of (FITS or not). My hope is to test the implementation against a mix of files of varying sizes/complexity/formats, files from the ESO archive, files produced by different compressing software and any "tricky" files that may exist. Essentially, I want to ensure I can read any files that uncompress or gunzip can.

  14. saimn commented on Mar 19, 2025

    @saimn
    Contributor
  15. kYwzor commented on Mar 31, 2025

    @kYwzor
    Member

    It seems I waited until the very last day to follow through with my "before the end of the March" plan :)

    Executive summary: I have now published the uncompresspy package and it performs better than all other pure Python packages. I will be providing a PR adding LZW file support to Astropy later today. I'll be sharing below what I've learned in these last couple of weeks (along with performance metrics).

    Preface

    I've found that, while there is an official specification for the Lempel–Ziv–Welch (LZW) algorithm, there is no official specification on how to apply it to a file or how the ".Z" extension files are supposed to be formatted (well, at least to my knowledge, I'd be interested in reading it if anyone finds it). This is particularly tricky because some of the quirks of LZW files are actually not part of the algorithm itself, but rather something that only really exists on the file format (namely specific flags on the header and "clear codes" aka "block mode"). That may be why I've found that nearly all LZW file decoders available online are based on one of two implementations:

    • Spencer W. Thomas et al. - First written over 40 years ago, it's the original implementation for Unix's compress, which essentially defined the de facto standard for .Z files. There's dozens of reimplementations of this algorithm, some examples are on (n)compress, gzip and FreeBSD
    • Mark Adler - As far as I can tell it was first written roughly 20 years ago, for zlib. Example code can be found on zlib's source code, but also on pigz and on StackExchange, both also written by Mark Adler. It generally performs better than the older implementation.

    Available packages

    Given all this, I decided it was best to cross check my implementation against the output (and performance) of multiple command line tools and Python packages. Starting with the Python packages, I went through all the relevant GitHub/PyPI projects I could find, and here's a summary:

    Github link PyPI name First commit Latest Commit Implementation Dependencies Based on Status
    python-unlzw unlzw 8 years ago 8 years ago C-based cffi, setuptools Mark Adler Broken1
    unlzw 2 7 years ago 7 years ago Pure Python None Mark Adler Working
    lzw3 lzw3 6 years ago 6 years ago Pure Python None Seems to be original code Broken3
    lzwfile lzwfile 6 years ago 5 years ago Pure Python None Mark Adler Working
    unlzw3 unlzw3 5 years ago 3 months ago Pure Python None Mark Adler Working
    ncompress ncompress 3 years ago 5 months ago C++-based scikit-build-core, nanobind Spencer W. Thomas et al. Working
    pyunixlzw pyunixlzw 3 months ago 1 month ago Pure Python None Spencer W. Thomas et al. Working
    uncompresspy uncompresspy Today Today Pure Python None Me! Working

    With the exception of uncompresspy, all of these packages force the input file to be decompressed entirely, which means 1) lazy/on-demand decompression is not possible and 2) the entire decompressed output must be held in memory. On top of that, with the exception of uncompresspy and ncompress, all of them also must hold the input file entirely in memory. uncompresspy is also the only package to provide a file-like interface for LZW files. On a small note, uncompresspy does not provide functions for compression, which a couple of these do (not really my aim here).

    Besides Python packages, a common alternative is to call a shell command via Python and capture the output. This works, but has the disadvantage of not really being cross-platform and requiring the user to install the specific package being used. Regardless, for comparison purposes, I've also collected metrics for uncompress (provided by (n)compress), gunzip and pigz.

    Performance comparison

    Some notes on the testing

    • All testing has been performed on the same Intel Xeon W-2295 machine running Debian 12 (bookworm), with 512GB of DDR4 memory.
    • All testing has been performed on Python 3.13 on the latest package versions available on PyPI at the time of writing. The shell commands were using the latest version of their respective Debian Bookworm repos.
    • The metrics shown are a mean of 5+ runs, with the error bars representing the standard deviation. Timing was calculated with the timeit module.
    • The timing includes the opening/reading of the input file.
    • Except where noted, all results are for full decompression of the file.
    • The outputs of all runs were compared to ensure the results are exactly the same. Except where noted, all the decompression results were the same.
    • Shell commands were called via subprocess.run with the options "shell=True, capture_output=True". This process implies a small overhead that the Python packages don't really have to go through, so it's not exactly apples to apples. However I believe it's a fair comparison in the sense that.
    • The naming on the plots follows the PyPI name for the Python packages (NOT the GitHub name, which sometimes differs) and the command name for the shell commands.
    • I'm providing metrics for all packages/commands mentioned above with the exception of lzw3 (it's broken), the predecessor of unlzw3 (it's not on PyPI and it's fully replaced by unlzw3 for all purposes) and pyunixlzw (it is so slow that, at the time of writing, it has been decompressing 10GB of zeros for 11+ hours; I might update the comment if I eventually get usable results. EDIT: Short update, I've got the results for pyunixlzw now and overall it's the slowest of all tested packages, I won't update the plots to include it because there's really no point. It performs decently on poorly compressed files (rate <1, trades blows with lzwfile/unlzw3), but scales really really badly with very high compression rates).

    All zeros files

    Image
    Files created via dd if=/dev/zero, i.e. files where the entire uncompressed content consists of repeated "0"s. This is by far the best-case scenario for uncompresspy; it's the only scenario where it beats every other package/command including all C-based ones. This is because the implementation of uncompresspy is extremely fast at dealing with greatly compressed files and LZW compresses the most when the entire file has the exact same character repeated. Obviously, this is a synthetic benchmark and not a realistic case.

    Random files

    Image
    Files created via dd if=/dev/urandom, i.e. files where the uncompressed content is entirely random. This is the worst-case scenario for uncompresspy as the compression ratio is terrible (less than 1, i.e. we're increasing storage usage rather than saving space). However, even in such a scenario, uncompresspy still clearly beats all other pure Python packages (though it's handily beat by C/C++-based alternatives). This is also an unrealistic synthetic benchmark, so let's look at real-world files.

    ESO FITS files

    Image
    These are all FITS files produced by ESO. The VISIR file was sourced from the link at the very top of this ticket; it's a simple FITS file containing just one extension with an image. The ERIAO and GALAC files are AO telemetry files in the AOT format (which I am the developer of), available on the ESO Archive; these are very complex files with many binary tables and images. The GRAV file is a interferometry telemetry file produced by the GRAVITY instrument (I sourced it directly from Chile as part of my work on GRAVITY+); this is also a file with many binary tables and images.

    Once again we can see that uncompresspy is the best performing pure Python package, but it lags behind C/C++ based implementations. We can also notice the previously seen behavior where its relative performance improves with higher compression ratios. However, the picture really changes if we only want a part of the FITS file:

    Image

    Because uncompresspy allows for a partial decompression (unlike the others) it can provide a huge speedup versus every other solution in instances where we only want e.g. the primary header (represented in the plot as "uncompresspy(header)") or the first extension (represented in the plot as "uncompresspy(data)") of a very large file. These two metrics were collected via fits.getheader(filename) and fits.getdata(filename, 1) respectively (using a branch I've created for Astropy with LZW support) and they are compared vs the other packages doing a full decompression (omitted the other pure Python packages here because they blow out the scale). It's easy to imagine a scenario where a user has a set of large files and wants to check the header for each of them before picking one, and in that case, uncompresspy provides a clear advantage above every alternative. Obviously, the plot is not an apples to apples comparison, but the point is that you could not achieve a similar effect with the other packages, as they do not allow for partial decompression. The effect here is less notable on VISIR because it's a smaller file that only contains a single extension (and thus fits.getdata(filename, 1) essentially forces us to decompress the entire file anyway).

    Tricky files

    Image

    For the comparison here the performance isn't really the point, but rather if the package was successful at decompressing or not. That said, these smaller files show the interesting effects of the overhead when piping commands from shell.

    • empty.txt is the smallest possible LZW file, i.e., a file that contains absolutely nothing. While lzwfile was tripped up by this edge case, all others managed to successfully return a 0-length bytes object.
    • igsg2260.15i was taken from a GitHub issue on another package. Unsure what the original source is but appears to be Ionosphere data from ESA(?). This file is interesting because at the end it contains a partial code (i.e. it starts a new code but does not finish it). This could indicate a truncated file but it could just be an implementation detail on a uncommon compressor. Most packages/commands tested throw an error and stop decompressing, however uncompress, gunzip and ncompress decompress the file silently. For uncompresspy I've opted for a middle ground, where it still returns the decompressed file, but throws out a warning indicating that the file may be truncated.
    • httpd-2.1.3-beta.tar was taken from a comment on another GitHub issue. It comes originally from Apache's httpd archive. In this file the alignment bits after a CLEAR code are not zero (like they typically are in files created by compress), but rather random uninitialized data. All packages seem to handle this just fine.
    • random100M_09.dat is a file that I created via 100 MB of /dev/urandom random data, and then used the compress -b 9 command to force it to a bit size of 9 (i.e. the minimum possible). Oddly enough, the file created in this manner could not be opened by uncompress, nor by any package except ncompress and uncompresspy; the latter two agree on the data contents and do not throw any warnings, but the data contents are only partially recovered. This is a weird case that I'm still trying to understand, I will try to get a final answer on this in the coming days, but from early investigations it seems to be a very long standing bug in compress which just creates a corrupt file. Compressing with any bit size from 10 to 16 resulted in files that could be properly read by every package.

    Key takeaways

    • uncompresspy has successfully decoded every valid LZW file I could find, including the ones discussed above but also many others I've found in the wild (omitted for the sake of brevity).
    • I have not tested uncompresspy for files that do not use block mode as I haven't managed to source any such file and compress seems unable to create them. However, I believe the implementation would still work for non-block mode files (to be demonstrated/fixed if someone can point me to such a file).
    • uncompresspy significantly outperforms every other pure Python package in full decompression.
    • unlzw is the best performing Python package overall if the proper fix is applied, performing on par with the fastest shell command (pigz, which makes sense as they share implementation). However, ncompress is the best performing working Python package (i.e. without requiring fixes).
    • uncompresspy is the only package that provides lazy/partial decompression and a file-like interface, allowing seamless integration with Astropy; this enables extremely fast reads of file headers (for large files this can be multiple thousands of times faster than the next fastest method). I will provide a PR for this functionality soon.

    Footnotes

    1. It has a memory leak and cuts the stream short if it finds a null char (which will happen with basically all FITS files). It also fails to build properly from Python 3.12 onwards. For testing sake, I used the patch provided in a pull request which fixes the first two issues and built the package manually. ↩

    2. Technically not on PyPI, but unlzw3 is just a fork with modernization, very similar implementation. ↩

    3. Broken in Python 3.10+, "cannot import name 'Callable' from 'collections'". If we remove the import it still creates broken files. I couldn't get it to work. ↩

  16. saimn commented on Apr 1, 2025

    @saimn
    Contributor

    Thanks for the detailed analysis @kYwzor, great job !
    Having support for partial read is great, as you say it can be useful to display the primary header. But to uncompress entire files the overhead of pure python vs c/c++ is not negligible. Did you check (or could you) if with your code uncompressing the full file at once is faster than uncompressing small blocks while the file is read ? This is the case with gzip (#14855) and lead to #15501.

  17. kYwzor commented on Apr 1, 2025

    @kYwzor
    Member

    @saimn thanks. I agree the Python overhead is significant. I spent a long time profiling and optimizing the code but even with all that effort pigz/unlzw are still ~10 to ~20 times faster than uncompresspy on a typical FITS file. There might still be room for some micro optimizations in my code but at this point I'd be very surprised to get even a 1.5x improvement on a single thread, much less close the gap entirely (side comment, even though LZW isn't really designed for parallelism, we could probably squeeze a bit more performance by decoding the bitstream on a separate thread from the construction of the dictionary and output, or maybe even try to split the decoding into different blocks... but even with all that complexity we would definitely not close the existing gap).

    Regarding decompressing in memory, with the exception of the metrics I show for fits.getheader(filename) and fits.getdata(filename, 1) (i.e. the second to last image), all plots are for full file decompression at once without any Astropy/io.fits overhead (simply a LZWFile.read(-1)). So decompressing the entire file in memory with the decompress_in_memory would yield similar results to what we already see in the plots (presumably slightly worse given the Astropy overhead). In other words, if we need significantly better performance, I don't think it's possible with a pure Python package.

    Despite all that, I believe that, as-is, the package is still very much usable for files up to ~100MB and okay-ish up to ~500MB (and then it starts to get a bit silly with 1min+ decompression times). I would argue this range covers a lot of use-cases and therefore it's an improvement over the current state of not supporting LZW files at all, thus it's worth chasing the PR even if sub-optimal.

    I think it's clear that the end goal is to have something that allows partial decompression + exposes a file-like interface + has a performance similar to pigz. I'm convinced this is very much achievable with a C/C++-based approach, but the implementation is tricky (particularly when it comes to managing the state of the decoder between partial decompressions). I would be interested in eventually exploring this, but I can't promise a timeline right now. In the meantime I think this is a step in the right direction.

  18. saimn commented on Apr 2, 2025

    @saimn
    Contributor

    all plots are for full file decompression at once without any Astropy/io.fits overhead

    Ok, so yeah might be worse when reading a file with many extensions (but then decompress_in_memory=True can be used).
    But I agree it's already a good first step, first we need something that works and then interested people can try to improve performance if this is important for their workflow.
    So thanks again, and I will look at the PR soon.

  19. kYwzor commented on Apr 9, 2025

    @kYwzor
    Member

    I had an informal exchange with an ex-member of the archive team regarding the frequency of these files on the ESO archive (this was a point of discussion early in this issue). I was told that the "vast majority of raw files" is compressed in this format, though there are exceptions mostly in newer files. The decision whether to use this compression or not is apparently done per-instrument, particularly they seem to avoid using this compression when the FITS file itself is already tile-compressed (and a few instruments have their files compressed with gzip). This is not an "official" ESO response (still attempting to get one) but I thought I'd leave it here for context.

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