-
Notifications
You must be signed in to change notification settings - Fork 8
Add support for rustfits fits backend. #177
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 6 commits
1927130
0916cf4
9b49fef
69f5cc1
8cb0a52
824652a
9d262a8
732b7e1
6e8ec6d
bf57865
24509ca
28fdc5e
20df8e2
228d526
bb81f24
ef105b4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -34,3 +34,5 @@ jobs: | |
| pytest | ||
| pip install fitsio>=1.0.5 | ||
| pytest | ||
| pip install rustfits | ||
| pytest | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,16 +1,25 @@ | ||
| import copy | ||
| import numpy as np | ||
| import mmap | ||
| from .utils import is_integer_value | ||
| # We need this for compression before a newer version of fitsio arrives | ||
| import astropy.io.fits as fits | ||
|
|
||
| use_rustfits = False | ||
| use_fitsio = False | ||
| try: | ||
| import fitsio | ||
| use_fitsio = True | ||
| import rustfits | ||
| use_rustfits = True | ||
| except ImportError: | ||
| pass | ||
|
|
||
| if not use_rustfits: | ||
| try: | ||
| import fitsio | ||
| use_fitsio = True | ||
| except ImportError: | ||
| pass | ||
|
|
||
|
|
||
| _image_bitpix2npy = { | ||
| 8: 'u1', | ||
|
|
@@ -29,12 +38,12 @@ | |
| 'ZPCOUNT', 'ZGCOUNT', 'ZTILE1', 'ZCMPTYPE', | ||
| 'ZNAME1', 'ZVAL1', 'ZQUANTIZ', | ||
| 'SIMPLE', 'BITPIX', 'NAXIS', 'NAXIS1', 'NAXIS2', | ||
| 'PCOUNT', 'GCOUNT'] | ||
| 'PCOUNT', 'GCOUNT', 'XTENSION'] | ||
|
|
||
|
|
||
| class HealSparseFits(object): | ||
| """ | ||
| Wrapper class to handle fitsio or astropy.io.fits | ||
| Wrapper class to handle rustfits, fitsio, or astropy.io.fits | ||
| """ | ||
| def __init__(self, filename, mode='r'): | ||
| """ | ||
|
|
@@ -54,7 +63,19 @@ def __init__(self, filename, mode='r'): | |
| self._filename = filename | ||
| self._mode = mode | ||
|
|
||
| if use_fitsio: | ||
| if use_rustfits: | ||
| if mode == "r": | ||
| rustfits_mode = "r" | ||
| elif mode == "rw": | ||
| rustfits_mode = "r+" | ||
|
|
||
| self.fits_object = rustfits.FITS(str(filename), mode=rustfits_mode) | ||
|
|
||
| try: | ||
| _ = self.fits_object[0] | ||
| except ValueError: | ||
| raise IOError("File %s does not appear to be a fits file." % (filename)) | ||
| elif use_fitsio: | ||
| self.fits_object = fitsio.FITS(filename, mode=mode) | ||
| else: | ||
| if mode == 'r': | ||
|
|
@@ -80,6 +101,7 @@ def read_ext_header(self, extension): | |
| if use_fitsio: | ||
| return self.fits_object[extension].read_header() | ||
| else: | ||
| # This works for rustfits and astropy fits. | ||
| return self.fits_object[extension].header | ||
|
|
||
| def get_ext_dtype(self, extension): | ||
|
|
@@ -95,9 +117,15 @@ def get_ext_dtype(self, extension): | |
| ------- | ||
| dtype : `np.dtype` | ||
| """ | ||
| if use_fitsio: | ||
| if use_rustfits: | ||
| hdu = self.fits_object[extension] | ||
| if hdu.get_exttype() == 'IMAGE_HDU': | ||
| if self.ext_is_image(extension): | ||
| return hdu.dtype.str | ||
| else: | ||
| return hdu.dtype | ||
| elif use_fitsio: | ||
| hdu = self.fits_object[extension] | ||
| if self.ext_is_image(extension): | ||
| return _image_bitpix2npy[hdu.get_info()['img_equiv_type']] | ||
| else: | ||
| return self.fits_object[extension].get_rec_dtype()[0] | ||
|
|
@@ -126,7 +154,7 @@ def read_ext_data(self, extension, row_range=None, col_range=None): | |
| ------- | ||
| data : `np.ndarray` | ||
| """ | ||
| if use_fitsio: | ||
| if use_rustfits or use_fitsio: | ||
| hdu = self.fits_object[extension] | ||
| if row_range is None: | ||
| return hdu.read() | ||
|
|
@@ -169,7 +197,9 @@ def ext_is_image(self, extension): | |
| is_image : `bool` | ||
| """ | ||
| hdu = self.fits_object[extension] | ||
| if use_fitsio: | ||
| if use_rustfits: | ||
| return isinstance(hdu, (rustfits.CompressedImageHDU, rustfits.ImageHDU)) | ||
| elif use_fitsio: | ||
| if hdu.get_exttype() == 'IMAGE_HDU': | ||
| return True | ||
| else: | ||
|
|
@@ -192,7 +222,14 @@ def append_extension(self, extension, data): | |
| raise RuntimeError("Appending only allowed in rw mode") | ||
|
|
||
| hdu = self.fits_object[extension] | ||
| if use_fitsio: | ||
| if use_rustfits: | ||
| if hasattr(hdu, 'extend'): | ||
| # We can append | ||
| hdu.extend(data) | ||
| else: | ||
| firstrow = (hdu.get_dims()[0], ) | ||
| hdu.write(data, start=firstrow) | ||
| elif use_fitsio: | ||
| if hasattr(hdu, 'append'): | ||
| # A recarray that we can append to | ||
| hdu.append(data) | ||
|
|
@@ -260,7 +297,26 @@ def _write_filename(filename, c_hdr, s_hdr, cov_index_map, sparse_map, | |
| _tile_shape = (compress_tilesize, ) | ||
| s_hdr['RESHAPED'] = False | ||
|
|
||
| if use_fitsio and integer_map: | ||
| if use_rustfits: | ||
| with rustfits.FITS(str(filename), mode="w+") as f: | ||
| f.write_image(cov_index_map, extname=c_hdr["EXTNAME"], header=c_hdr) | ||
|
|
||
| if compress: | ||
| if compression == "GZIP_2": | ||
| comp = rustfits.Gzip2(tile_shape=_tile_shape) | ||
| elif compression == "RICE_1": | ||
| comp = rustfits.Rice1(tile_shape=_tile_shape) | ||
|
|
||
| f.write_image( | ||
| _sparse_map, | ||
| extname=s_hdr["EXTNAME"], | ||
| header=s_hdr, | ||
| compress=comp, | ||
| ) | ||
| else: | ||
| f.write(sparse_map, extname=s_hdr["EXTNAME"], header=s_hdr) | ||
|
|
||
| elif use_fitsio and integer_map: | ||
| # Preferred because it is faster for integer writes. | ||
| # Floating point writing with compression has only just | ||
| # been fixed and I don't want to put a lower limit on | ||
|
|
@@ -355,6 +411,14 @@ def _make_header(metadata, force_astropy=False): | |
| ------- | ||
| header : `fitsio.FITSHDR` or `astropy.io.fits.Header` | ||
| """ | ||
| if use_rustfits and not force_astropy: | ||
| if metadata is None: | ||
| return {} | ||
| else: | ||
| hdr = copy.copy(metadata) | ||
| for reserved in FITS_RESERVED: | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
rustfits provides Note, if this metadata was itself created from a rustfits It probably makes sense to provide a method to clean a dict of protected keys so you don't need the loop.
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Because of the way that the tests do reading/writing, and there's no way to directly create a rustfits header in python, sometimes this code gets a rustfits header and sometimes a dict. But I will make use of the
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hmm. I don't want to loop over all keys ... just the protected ones. I just don't know how to do this without a bunch more special cases.
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There is no need to change what you are doing, I was just giving information, sorry to imply otherwise Here is what rustfits does |
||
| hdr.pop(reserved, None) | ||
| return hdr | ||
| if use_fitsio and not force_astropy: | ||
| hdr = fitsio.FITSHDR(metadata) | ||
| else: | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -25,7 +25,7 @@ def _read_coverage_fits(coverage_class, filename_or_fits): | |
|
|
||
| try: | ||
| cov_index_map = fits.read_ext_data('COV') | ||
| except (OSError, KeyError): | ||
| except (OSError, KeyError, ValueError): | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What is the situation here? It is a fits file but doesn't have the 'COV' extension? |
||
| raise RuntimeError("File is not a HealSparseMap") | ||
|
|
||
| s_hdr = fits.read_ext_header('SPARSE') | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -56,9 +56,10 @@ def _read_map(healsparse_class, filename, nside_coverage=None, pixels=None, head | |
| fits = HealSparseFits(filename) | ||
| is_fits_file = True | ||
| fits.close() | ||
| except (OSError, UnicodeDecodeError): | ||
| except (OSError, UnicodeDecodeError, ValueError): | ||
| pass | ||
| # UnicodeDecodeError occurs when trying to read an hdf5 file as if it's FITS | ||
| # ValueError occurs from rustfits when trying to read an hdf5 file as if it's FITS | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What actual error is raised? I'm guessing it is ValueError because it tries to parse the primary header and finds the wrong values in the first bytes, but I want to see if a different error might be more appropriate to raise.
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So the following code snippet raises
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. rustfits.FITS should do eager header parsing on open. It is true that fitsio was lazy
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. But it doesn’t parse the header on an empty file?
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. sounds like a bug, investigating
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this is fixed and will be in the next release |
||
|
|
||
| if not is_fits_file: | ||
| is_parquet_file = check_parquet_dataset(filename) | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You can just check
rustfits.ImageHDU