Skip to content

Commit 8211036

Browse files
authored
Merge pull request #818 from havok2063/jwstc1d
Adds support for JWST c1d spectroscopic files
2 parents 1f6eded + 3509982 commit 8211036

File tree

2 files changed

+172
-44
lines changed

2 files changed

+172
-44
lines changed

specutils/io/default_loaders/jwst_reader.py

Lines changed: 100 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from astropy.table import Table
44
from astropy.io import fits
55
from astropy.nddata import StdDevUncertainty
6+
import logging
67
import numpy as np
78
import asdf
89
from gwcs.wcstools import grid_from_bounding_box
@@ -14,23 +15,51 @@
1415

1516
__all__ = ["jwst_x1d_single_loader", "jwst_x1d_multi_loader"]
1617

18+
log = logging.getLogger(__name__)
1719

18-
def identify_jwst_x1d_fits(origin, *args, **kwargs):
20+
21+
def _identify_spec1d_fits(origin, extname, *args, **kwargs):
22+
""" Generic spec 1d identifier function """
23+
is_jwst = _identify_jwst_fits(*args)
24+
with read_fileobj_or_hdulist(*args, memmap=False, **kwargs) as hdulist:
25+
return (is_jwst and extname in hdulist and (extname, 2) not in hdulist)
26+
27+
28+
def _identify_spec1d_multi_fits(origin, extname, *args, **kwargs):
1929
"""
20-
Check whether the given file is a JWST x1d spectral data product.
30+
Check whether the given file is a JWST c1d/x1d spectral data product with many slits.
2131
"""
2232
is_jwst = _identify_jwst_fits(*args)
2333
with read_fileobj_or_hdulist(*args, memmap=False, **kwargs) as hdulist:
24-
return (is_jwst and 'EXTRACT1D' in hdulist and ('EXTRACT1D', 2) not in hdulist)
34+
return is_jwst and (extname, 2) in hdulist
35+
36+
37+
def identify_jwst_c1d_fits(origin, *args, **kwargs):
38+
"""
39+
Check whether the given file is a JWST c1d spectral data product.
40+
"""
41+
return _identify_spec1d_fits(origin, 'COMBINE1D', *args, **kwargs)
42+
43+
44+
def identify_jwst_c1d_multi_fits(origin, *args, **kwargs):
45+
"""
46+
Check whether the given file is a JWST c1d spectral data product with many slits.
47+
"""
48+
return _identify_spec1d_multi_fits(origin, 'COMBINE1D', *args, **kwargs)
49+
50+
51+
def identify_jwst_x1d_fits(origin, *args, **kwargs):
52+
"""
53+
Check whether the given file is a JWST x1d spectral data product.
54+
"""
55+
return _identify_spec1d_fits(origin, 'EXTRACT1D', *args, **kwargs)
2556

2657

2758
def identify_jwst_x1d_multi_fits(origin, *args, **kwargs):
2859
"""
2960
Check whether the given file is a JWST x1d spectral data product with many slits.
3061
"""
31-
is_jwst = _identify_jwst_fits(*args)
32-
with read_fileobj_or_hdulist(*args, memmap=False, **kwargs) as hdulist:
33-
return is_jwst and ('EXTRACT1D', 2) in hdulist
62+
return _identify_spec1d_multi_fits(origin, 'EXTRACT1D', *args, **kwargs)
3463

3564

3665
def identify_jwst_s2d_fits(origin, *args, **kwargs):
@@ -75,6 +104,50 @@ def _identify_jwst_fits(*args):
75104
return False
76105

77106

107+
@data_loader("JWST c1d", identifier=identify_jwst_c1d_fits, dtype=Spectrum1D,
108+
extensions=['fits'])
109+
def jwst_c1d_single_loader(file_obj, **kwargs):
110+
"""
111+
Loader for JWST c1d 1-D spectral data in FITS format
112+
113+
Parameters
114+
----------
115+
filename : str
116+
The path to the FITS file
117+
118+
Returns
119+
-------
120+
Spectrum1D
121+
The spectrum contained in the file.
122+
"""
123+
spectrum_list = _jwst_spec1d_loader(file_obj, extname='COMBINE1D', **kwargs)
124+
if len(spectrum_list) == 1:
125+
return spectrum_list[0]
126+
else:
127+
raise RuntimeError(f"Input data has {len(spectrum_list)} spectra. "
128+
"Use SpectrumList.read() instead.")
129+
130+
131+
@data_loader("JWST c1d multi", identifier=identify_jwst_c1d_multi_fits,
132+
dtype=SpectrumList, extensions=['fits'])
133+
def jwst_c1d_multi_loader(file_obj, **kwargs):
134+
"""
135+
Loader for JWST x1d 1-D spectral data in FITS format
136+
137+
Parameters
138+
----------
139+
file_obj: str, file-like, or HDUList
140+
FITS file name, object (provided from name by Astropy I/O Registry),
141+
or HDUList (as resulting from astropy.io.fits.open()).
142+
143+
Returns
144+
-------
145+
SpectrumList
146+
A list of the spectra that are contained in the file.
147+
"""
148+
return _jwst_spec1d_loader(file_obj, extname='COMBINE1D', **kwargs)
149+
150+
78151
@data_loader("JWST x1d", identifier=identify_jwst_x1d_fits, dtype=Spectrum1D,
79152
extensions=['fits'])
80153
def jwst_x1d_single_loader(file_obj, **kwargs):
@@ -91,7 +164,7 @@ def jwst_x1d_single_loader(file_obj, **kwargs):
91164
Spectrum1D
92165
The spectrum contained in the file.
93166
"""
94-
spectrum_list = _jwst_x1d_loader(file_obj, **kwargs)
167+
spectrum_list = _jwst_spec1d_loader(file_obj, extname='EXTRACT1D', **kwargs)
95168
if len(spectrum_list) == 1:
96169
return spectrum_list[0]
97170
else:
@@ -116,33 +189,38 @@ def jwst_x1d_multi_loader(file_obj, **kwargs):
116189
SpectrumList
117190
A list of the spectra that are contained in the file.
118191
"""
119-
return _jwst_x1d_loader(file_obj, **kwargs)
192+
return _jwst_spec1d_loader(file_obj, extname='EXTRACT1D', **kwargs)
120193

121194

122-
def _jwst_x1d_loader(file_obj, **kwargs):
195+
def _jwst_spec1d_loader(file_obj, extname='EXTRACT1D', **kwargs):
123196
"""Implementation of loader for JWST x1d 1-D spectral data in FITS format
124197
125198
Parameters
126199
----------
127200
file_obj: str, file-like, or HDUList
128201
FITS file name, object (provided from name by Astropy I/O Registry),
129202
or HDUList (as resulting from astropy.io.fits.open()).
203+
extname: str
204+
The name of the science extension. Either "COMBINE1D" or "EXTRACT1D". By default "EXTRACT1D".
130205
131206
Returns
132207
-------
133208
SpectrumList
134209
A list of the spectra that are contained in the file.
135210
"""
136211

212+
if extname not in ['COMBINE1D', 'EXTRACT1D']:
213+
raise ValueError('Incorrect extname given for 1d spectral data.')
214+
137215
spectra = []
138216

139217
with read_fileobj_or_hdulist(file_obj, memmap=False, **kwargs) as hdulist:
140218

141219
primary_header = hdulist["PRIMARY"].header
142220

143221
for hdu in hdulist:
144-
# Read only the BinaryTableHDUs named EXTRACT1D and SCI
145-
if hdu.name != 'EXTRACT1D':
222+
# Read only the BinaryTableHDUs named COMBINE1D/EXTRACT1D and SCI
223+
if hdu.name != extname:
146224
continue
147225

148226
data = Table.read(hdu)
@@ -155,18 +233,26 @@ def _jwst_x1d_loader(file_obj, **kwargs):
155233
# SRCTYPE used to be in primary header, but was moved around. As
156234
# per most recent pipeline definition, it should be in the
157235
# EXTRACT1D extension.
236+
#
237+
# SRCTYPE should either be POINT or EXTENDED. In some cases, it is UNKNOWN
238+
# or missing. If that's the case, default to using POINT as the SRCTYPE.
239+
# Error out only when SRCTYPE is a bad value.
158240
srctype = None
159241
if "srctype" in hdu.header:
160-
srctype = hdu.header.get("srctype")
242+
srctype = hdu.header.get("srctype", None)
243+
244+
# checking if SRCTPYE is missing or UNKNOWN
245+
if not srctype or srctype == 'UNKNOWN':
246+
log.warning('SRCTYPE is missing or UNKNOWN in JWST x1d loader. '
247+
'Defaulting to srctype="POINT".')
248+
srctype = 'POINT'
161249

162250
if srctype == "POINT":
163251
flux = Quantity(data["FLUX"])
164252
uncertainty = StdDevUncertainty(data["ERROR"])
165-
166253
elif srctype == "EXTENDED":
167254
flux = Quantity(data["SURF_BRIGHT"])
168255
uncertainty = StdDevUncertainty(hdu.data["SB_ERROR"])
169-
170256
else:
171257
raise RuntimeError(f"Keyword SRCTYPE is {srctype}. It should "
172258
"be 'POINT' or 'EXTENDED'. Can't decide between `flux` and "

specutils/io/default_loaders/tests/test_jwst_reader.py

Lines changed: 72 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -13,15 +13,15 @@
1313
from specutils import Spectrum1D, SpectrumList
1414

1515

16-
# The x1d reader tests --------------------------
16+
# The c1d/x1d reader tests --------------------------
1717

18-
def create_spectrum_hdu(data_len, srctype=None, ver=1):
18+
def create_spectrum_hdu(data_len, srctype=None, ver=1, name='EXTRACT1D'):
1919
"""Mock a JWST x1d BinTableHDU"""
2020
data = np.random.random((data_len, 5))
2121
table = Table(data=data, names=['WAVELENGTH', 'FLUX', 'ERROR', 'SURF_BRIGHT',
2222
'SB_ERROR'])
2323

24-
hdu = fits.BinTableHDU(table, name='EXTRACT1D')
24+
hdu = fits.BinTableHDU(table, name=name)
2525
hdu.header['TUNIT1'] = 'um'
2626
hdu.header['TUNIT2'] = 'Jy'
2727
hdu.header['TUNIT3'] = 'Jy'
@@ -48,27 +48,46 @@ def x1d_single():
4848

4949

5050
@pytest.fixture(scope="function")
51-
def x1d_multi():
52-
"""Mock a JWST x1d multispec HDUList with 3 spectra"""
51+
def spec_single(request):
52+
"""Mock a JWST c1d/x1d HDUList with a single spectrum"""
53+
name = request.param
54+
hdulist = fits.HDUList()
55+
hdulist.append(fits.PrimaryHDU())
56+
hdulist["PRIMARY"].header["TELESCOP"] = ("JWST", "comment")
57+
# Add a BinTableHDU that contains spectral data
58+
hdulist.append(create_spectrum_hdu(100, 'POINT', ver=1, name=name))
59+
# Mock the ASDF extension
60+
hdulist.append(fits.BinTableHDU(name='ASDF'))
61+
62+
return hdulist
63+
64+
65+
@pytest.fixture(scope="function")
66+
def spec_multi(request):
67+
"""Mock a JWST c1d/x1d multispec HDUList with 3 spectra"""
68+
name = request.param
5369
hdulist = fits.HDUList()
5470
hdulist.append(fits.PrimaryHDU())
5571
hdulist["PRIMARY"].header["TELESCOP"] = "JWST"
5672
# Add a few BinTableHDUs that contain spectral data
57-
hdulist.append(create_spectrum_hdu(100, 'POINT', ver=1))
58-
hdulist.append(create_spectrum_hdu(120, 'EXTENDED', ver=2))
59-
hdulist.append(create_spectrum_hdu(110, 'POINT', ver=3))
73+
hdulist.append(create_spectrum_hdu(100, 'POINT', ver=1, name=name))
74+
hdulist.append(create_spectrum_hdu(120, 'EXTENDED', ver=2, name=name))
75+
hdulist.append(create_spectrum_hdu(110, 'POINT', ver=3, name=name))
6076
# Mock the ASDF extension
6177
hdulist.append(fits.BinTableHDU(name='ASDF'))
6278

6379
return hdulist
6480

6581

66-
def test_jwst_x1d_multi_reader(tmpdir, x1d_multi):
67-
"""Test SpectrumList.read for JWST x1d multi data"""
82+
@pytest.mark.parametrize('spec_multi, format',
83+
[('EXTRACT1D', 'JWST x1d multi'),
84+
('COMBINE1D', 'JWST c1d multi')], indirect=['spec_multi'])
85+
def test_jwst_1d_multi_reader(tmpdir, spec_multi, format):
86+
"""Test SpectrumList.read for JWST c1d/x1d multi data"""
6887
tmpfile = str(tmpdir.join('jwst.fits'))
69-
x1d_multi.writeto(tmpfile)
88+
spec_multi.writeto(tmpfile)
7089

71-
data = SpectrumList.read(tmpfile, format='JWST x1d multi')
90+
data = SpectrumList.read(tmpfile, format=format)
7291
assert type(data) is SpectrumList
7392
assert len(data) == 3
7493

@@ -80,20 +99,39 @@ def test_jwst_x1d_multi_reader(tmpdir, x1d_multi):
8099
assert data[2].shape == (110,)
81100

82101

83-
def test_jwst_x1d_single_reader(tmpdir, x1d_single):
102+
@pytest.mark.parametrize('spec_single, format',
103+
[('EXTRACT1D', 'JWST x1d'),
104+
('COMBINE1D', 'JWST c1d')], indirect=['spec_single'])
105+
def test_jwst_1d_single_reader(tmpdir, spec_single, format):
84106
"""Test Spectrum1D.read for JWST x1d data"""
85107
tmpfile = str(tmpdir.join('jwst.fits'))
108+
spec_single.writeto(tmpfile)
109+
110+
data = Spectrum1D.read(tmpfile, format=format)
111+
assert type(data) is Spectrum1D
112+
assert data.shape == (100,)
113+
114+
115+
@pytest.mark.parametrize("srctype", [None, "UNKNOWN"])
116+
def test_jwst_srctpye_defaults(tmpdir, x1d_single, srctype):
117+
""" Test """
118+
tmpfile = str(tmpdir.join('jwst.fits'))
119+
120+
# Add a spectrum with missing or UNKNOWN SRCTYPE (mutate the fixture)
121+
x1d_single['EXTRACT1D'].header['SRCTYPE'] == srctype
86122
x1d_single.writeto(tmpfile)
87123

88124
data = Spectrum1D.read(tmpfile, format='JWST x1d')
89125
assert type(data) is Spectrum1D
90126
assert data.shape == (100,)
127+
assert x1d_single['EXTRACT1D'].header['SRCTYPE'] == "POINT"
91128

92129

93-
def test_jwst_x1d_single_reader_no_format(tmpdir, x1d_single):
94-
"""Test Spectrum1D.read for JWST x1d data without format arg"""
130+
@pytest.mark.parametrize('spec_single', ['EXTRACT1D', 'COMBINE1D'], indirect=['spec_single'])
131+
def test_jwst_1d_single_reader_no_format(tmpdir, spec_single):
132+
"""Test Spectrum1D.read for JWST c1d/x1d data without format arg"""
95133
tmpfile = str(tmpdir.join('jwst.fits'))
96-
x1d_single.writeto(tmpfile)
134+
spec_single.writeto(tmpfile)
97135

98136
data = Spectrum1D.read(tmpfile)
99137
assert type(data) is Spectrum1D
@@ -102,10 +140,11 @@ def test_jwst_x1d_single_reader_no_format(tmpdir, x1d_single):
102140
assert data.spectral_axis.unit == u.um
103141

104142

105-
def test_jwst_x1d_multi_reader_no_format(tmpdir, x1d_multi):
106-
"""Test Spectrum1D.read for JWST x1d data without format arg"""
143+
@pytest.mark.parametrize('spec_multi', ['EXTRACT1D', 'COMBINE1D'], indirect=['spec_multi'])
144+
def test_jwst_1d_multi_reader_no_format(tmpdir, spec_multi):
145+
"""Test Spectrum1D.read for JWST c1d/x1d data without format arg"""
107146
tmpfile = str(tmpdir.join('jwst.fits'))
108-
x1d_multi.writeto(tmpfile)
147+
spec_multi.writeto(tmpfile)
109148

110149
data = SpectrumList.read(tmpfile)
111150
assert type(data) is SpectrumList
@@ -115,39 +154,42 @@ def test_jwst_x1d_multi_reader_no_format(tmpdir, x1d_multi):
115154
assert isinstance(item, Spectrum1D)
116155

117156

118-
def test_jwst_x1d_multi_reader_check_units(tmpdir, x1d_multi):
119-
"""Test units for Spectrum1D.read for JWST x1d data"""
157+
@pytest.mark.parametrize('spec_multi', ['EXTRACT1D', 'COMBINE1D'], indirect=['spec_multi'])
158+
def test_jwst_1d_multi_reader_check_units(tmpdir, spec_multi):
159+
"""Test units for Spectrum1D.read for JWST c1d/x1d data"""
120160
tmpfile = str(tmpdir.join('jwst.fits'))
121-
x1d_multi.writeto(tmpfile)
161+
spec_multi.writeto(tmpfile)
122162

123163
data = SpectrumList.read(tmpfile)
124164
assert data[0].unit == u.Jy
125165
assert data[1].unit == u.MJy / u.sr
126166
assert data[2].unit == u.Jy
127167

128168

129-
def test_jwst_x1d_reader_meta(tmpdir, x1d_single):
130-
"""Test that the Primary and EXTRACT1D extension headers are merged in meta"""
169+
@pytest.mark.parametrize('spec_single', ['EXTRACT1D', 'COMBINE1D'], indirect=['spec_single'])
170+
def test_jwst_1d_reader_meta(tmpdir, spec_single):
171+
"""Test that the Primary and COMBINE1D/EXTRACT1D extension headers are merged in meta"""
131172
tmpfile = str(tmpdir.join('jwst.fits'))
132-
x1d_single.writeto(tmpfile)
173+
spec_single.writeto(tmpfile)
133174

134175
data = Spectrum1D.read(tmpfile)
135176
assert ('TELESCOP', 'JWST') in data.meta['header'].items()
136177
assert ('SRCTYPE', 'POINT') in data.meta['header'].items()
137178

138179

139-
def test_jwst_x1d_single_reader_fail_on_multi(tmpdir, x1d_multi):
140-
"""Make sure Spectrum1D.read on JWST x1d with many spectra errors out"""
180+
@pytest.mark.parametrize('spec_multi', ['EXTRACT1D', 'COMBINE1D'], indirect=['spec_multi'])
181+
def test_jwst_1d_single_reader_fail_on_multi(tmpdir, spec_multi):
182+
"""Make sure Spectrum1D.read on JWST c1d/x1d with many spectra errors out"""
141183
tmpfile = str(tmpdir.join('jwst.fits'))
142-
x1d_multi.writeto(tmpfile)
184+
spec_multi.writeto(tmpfile)
143185

144186
with pytest.raises(IORegistryError):
145187
Spectrum1D.read(tmpfile)
146188

147189

148-
@pytest.mark.parametrize("srctype", [None, "UNKNOWN"])
190+
@pytest.mark.parametrize("srctype", ["BADVAL"])
149191
def test_jwst_reader_fail(tmpdir, x1d_single, srctype):
150-
"""Check that the reader fails when SRCTYPE is not set or is UNKNOWN"""
192+
"""Check that the reader fails when SRCTYPE is a BADVAL"""
151193
tmpfile = str(tmpdir.join('jwst.fits'))
152194
hdulist = x1d_single
153195
# Add a spectrum with bad SRCTYPE (mutate the fixture)

0 commit comments

Comments
 (0)