Repository navigation
More careful handling of masked values in FITS tables #11222
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
Changes from all commits
cf5745e
eabbc1e
0531121
58d0cb5
0fb52e3
3c8b706
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 |
|---|---|---|
|
|
@@ -6,6 +6,8 @@ | |
| import warnings | ||
| from copy import deepcopy | ||
|
|
||
| import numpy as np | ||
|
|
||
| from astropy.io import registry as io_registry | ||
| from astropy import units as u | ||
| from astropy.table import Table, serialize, meta, Column, MaskedColumn | ||
|
|
@@ -227,25 +229,30 @@ def read_table_fits(input, hdu=None, astropy_native=False, memmap=False, | |
| finally: | ||
| hdulist.close() | ||
|
|
||
| # Check if table is masked | ||
| masked = any(col.null is not None for col in table.columns) | ||
|
|
||
| # TODO: in future, it may make more sense to do this column-by-column, | ||
| # rather than via the structured array. | ||
|
|
||
| # In the loop below we access the data using data[col.name] rather than | ||
| # col.array to make sure that the data is scaled correctly if needed. | ||
| data = table.data | ||
|
|
||
| columns = [] | ||
| for col in data.columns: | ||
|
|
||
| # Set column data | ||
| if masked: | ||
| column = MaskedColumn(data=data[col.name], name=col.name, copy=False) | ||
| if col.null is not None: | ||
| column.set_fill_value(col.null) | ||
| column.mask[column.data == col.null] = True | ||
| # Check if column is masked. Here, we make a guess based on the | ||
| # presence of FITS mask values. For integer columns, this is simply | ||
| # the null header, for float and complex, the presence of NaN, and for | ||
| # string, empty strings. | ||
| masked = mask = False | ||
| if col.null is not None: | ||
| mask = col.array == col.null | ||
| # Return a MaskedColumn even if no elements are masked so | ||
|
Contributor
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. Does this make sense? Or should we always check if any masked values are present?
Contributor
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. The current solution makes sense. |
||
| # we roundtrip better. | ||
| masked = True | ||
| elif issubclass(col.dtype.type, np.inexact): | ||
| mask = np.isnan(col.array) | ||
| elif issubclass(col.dtype.type, np.character): | ||
| mask = col.array == b'' | ||
|
|
||
| if masked or np.any(mask): | ||
| column = MaskedColumn(data=data[col.name], name=col.name, | ||
| mask=mask, copy=False) | ||
| else: | ||
| column = Column(data=data[col.name], name=col.name, copy=False) | ||
|
|
||
|
|
@@ -260,7 +267,7 @@ def read_table_fits(input, hdu=None, astropy_native=False, memmap=False, | |
| columns.append(column) | ||
|
|
||
| # Create Table object | ||
| t = Table(columns, masked=masked, copy=False) | ||
| t = Table(columns, copy=False) | ||
|
|
||
| # TODO: deal properly with unsigned integers | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,12 +1,11 @@ | ||
| import os | ||
| import gc | ||
| import sys | ||
| import pathlib | ||
| import warnings | ||
|
|
||
| import pytest | ||
| import numpy as np | ||
| from numpy.testing import assert_allclose | ||
| from numpy.testing import assert_allclose, assert_array_equal | ||
|
|
||
| from astropy.io.fits.column import (_parse_tdisp_format, _fortran_to_python_format, | ||
| python_to_tdisp) | ||
|
|
@@ -168,31 +167,35 @@ def test_masked(self, tmpdir): | |
| t1.mask['c'] = [0, 1, 1, 0] | ||
| t1.write(filename, overwrite=True) | ||
| t2 = Table.read(filename) | ||
| assert t2.masked | ||
| assert equal_data(t1, t2) | ||
| assert np.all(t1['a'].mask == t2['a'].mask) | ||
| # Disabled for now, as there is no obvious way to handle masking of | ||
| # non-integer columns in FITS | ||
| # TODO: Re-enable these tests if some workaround for this can be found | ||
| # assert np.all(t1['b'].mask == t2['b'].mask) | ||
| # assert np.all(t1['c'].mask == t2['c'].mask) | ||
| assert np.all(t1['b'].mask == t2['b'].mask) | ||
| assert np.all(t1['c'].mask == t2['c'].mask) | ||
|
|
||
| def test_masked_nan(self, tmpdir): | ||
| @pytest.mark.parametrize('masked', [True, False]) | ||
| def test_masked_nan(self, masked, tmpdir): | ||
| filename = str(tmpdir.join('test_masked_nan.fits')) | ||
| data = np.array(list(zip([5.2, 8.4, 3.9, 6.3], | ||
| [2.3, 4.5, 6.7, 8.9])), | ||
| dtype=[('a', np.float64), ('b', np.float32)]) | ||
| t1 = Table(data, masked=True) | ||
| t1.mask['a'] = [1, 0, 1, 0] | ||
| t1.mask['b'] = [1, 0, 0, 1] | ||
| a = np.ma.MaskedArray([5.25, 8.5, 3.75, 6.25], mask=[1, 0, 1, 0]) | ||
| b = np.ma.MaskedArray([2.5, 4.5, 6.75, 8.875], mask=[1, 0, 0, 1], dtype='f4') | ||
| t1 = Table([a, b], names=['a', 'b'], masked=masked) | ||
| t1.write(filename, overwrite=True) | ||
| t2 = Table.read(filename) | ||
| np.testing.assert_array_almost_equal(t2['a'], [np.nan, 8.4, np.nan, 6.3]) | ||
| np.testing.assert_array_almost_equal(t2['b'], [np.nan, 4.5, 6.7, np.nan]) | ||
| # assert t2.masked | ||
| # t2.masked = false currently, as the only way to determine whether a table is masked | ||
| # while reading is to check whether col.null is present. For float columns, col.null | ||
| # is not initialized | ||
| assert_array_equal(t2['a'].data, [np.nan, 8.5, np.nan, 6.25]) | ||
| assert_array_equal(t2['b'].data, [np.nan, 4.5, 6.75, np.nan]) | ||
| assert np.all(t1['a'].mask == t2['a'].mask) | ||
| assert np.all(t1['b'].mask == t2['b'].mask) | ||
|
|
||
| def test_masked_serialize_data_mask(self, tmpdir): | ||
| filename = str(tmpdir.join('test_masked_nan.fits')) | ||
| a = np.ma.MaskedArray([5.25, 8.5, 3.75, 6.25], mask=[1, 0, 1, 0]) | ||
| b = np.ma.MaskedArray([2.5, 4.5, 6.75, 8.875], mask=[1, 0, 0, 1]) | ||
| t1 = Table([a, b], names=['a', 'b']) | ||
| t1.write(filename, overwrite=True, serialize_method='data_mask') | ||
| t2 = Table.read(filename) | ||
| assert_array_equal(t2['a'].data, [5.25, 8.5, 3.75, 6.25]) | ||
| assert_array_equal(t2['b'].data, [2.5, 4.5, 6.75, 8.875]) | ||
| assert np.all(t1['a'].mask == t2['a'].mask) | ||
| assert np.all(t1['b'].mask == t2['b'].mask) | ||
|
|
||
| def test_read_from_fileobj(self, tmpdir): | ||
| filename = str(tmpdir.join('test_read_from_fileobj.fits')) | ||
|
|
@@ -493,9 +496,9 @@ def test_masking_regression_1795(): | |
| """ | ||
| t = Table.read(get_pkg_data_filename('data/tb.fits')) | ||
| assert np.all(t['c1'].mask == np.array([False, False])) | ||
| assert np.all(t['c2'].mask == np.array([False, False])) | ||
| assert np.all(t['c3'].mask == np.array([False, False])) | ||
| assert np.all(t['c4'].mask == np.array([False, False])) | ||
| assert not hasattr(t['c2'], 'mask') | ||
|
Contributor
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. A consequence of no longer returning a table with |
||
| assert not hasattr(t['c3'], 'mask') | ||
| assert not hasattr(t['c4'], 'mask') | ||
| assert np.all(t['c1'].data == np.array([1, 2])) | ||
| assert np.all(t['c2'].data == np.array([b'abc', b'xy '])) | ||
| assert_allclose(t['c3'].data, np.array([3.70000007153, 6.6999997139])) | ||
|
|
||
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.
I thought we should just do away with the masked table altogether, as it is more or less deprecated.