Skip to content

Commit 345c452

Browse files
authored
Implement 1D wavelength calibration classes (#162)
1 parent 587e38a commit 345c452

File tree

7 files changed

+407
-1
lines changed

7 files changed

+407
-1
lines changed

docs/conf.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -174,7 +174,8 @@
174174
{
175175
'astropy': ('https://docs.astropy.org/en/stable/', None),
176176
'ccdproc': ('https://ccdproc.readthedocs.io/en/stable/', None),
177-
'specutils': ('https://specutils.readthedocs.io/en/stable/', None)
177+
'specutils': ('https://specutils.readthedocs.io/en/stable/', None),
178+
'gwcs': ('https://gwcs.readthedocs.io/en/stable/', None)
178179
}
179180
)
180181
#

docs/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ Calibration
5050
.. toctree::
5151
:maxdepth: 1
5252

53+
wavelength_calibration.rst
5354
extinction.rst
5455
specphot_standards.rst
5556

docs/wavelength_calibration.rst

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
.. _wavelength_calibration:
2+
3+
Wavelength Calibration
4+
======================
5+
6+
Wavelength calibration is currently supported for 1D spectra. Given a list of spectral
7+
lines with known wavelengths and estimated pixel positions on an input calibration
8+
spectrum, you can currently use ``specreduce`` to:
9+
10+
#. Fit an ``astropy`` model to the wavelength/pixel pairs to generate a spectral WCS
11+
solution for the dispersion.
12+
#. Apply the generated spectral WCS to other `~specutils.Spectrum1D` objects.
13+
14+
1D Wavelength Calibration
15+
-------------------------
16+
17+
The `~specreduce.wavelength_calibration.WavelengthCalibration1D` class can be used
18+
to fit a dispersion model to a list of line positions and wavelengths. Future development
19+
will implement catalogs of known lamp spectra for use in matching observed lines. In the
20+
example below, the line positions (``pixel_centers``) have already been extracted from
21+
``lamp_spectrum``::
22+
23+
import astropy.units as u
24+
from specreduce import WavelengthCalibration1D
25+
pixel_centers = [10, 22, 31, 43]
26+
wavelengths = [5340, 5410, 5476, 5543]*u.AA
27+
test_cal = WavelengthCalibration1D(lamp_spectrum, line_pixels=pixel_centers,
28+
line_wavelengths=wavelengths)
29+
calibrated_spectrum = test_cal.apply_to_spectrum(science_spectrum)
30+
31+
The example above uses the default model (`~astropy.modeling.functional_models.Linear1D`)
32+
to fit the input spectral lines, and then applies the calculated WCS solution to a second
33+
spectrum (``science_spectrum``). Any other 1D ``astropy`` model can be provided as the
34+
input ``model`` parameter to the `~specreduce.wavelength_calibration.WavelengthCalibration1D`.
35+
In the above example, the model fit and WCS construction is all done as part of the
36+
``apply_to_spectrum()`` call, but you could also access the `~gwcs.wcs.WCS` object itself
37+
by calling::
38+
39+
test_cal.wcs
40+
41+
The calculated WCS is a cached property that will be cleared if the ``line_list``, ``model``,
42+
or ``input_spectrum`` properties are updated, since these will alter the calculated dispersion
43+
fit.
44+
45+
You can also provide the input pixel locations and wavelengths of the lines as an
46+
`~astropy.table.QTable` with (at minimum) columns ``pixel_center`` and ``wavelength``,
47+
using the ``matched_line_list`` input argument::
48+
49+
from astropy.table import QTable
50+
pixels = [10, 20, 30, 40]*u.pix
51+
wavelength = [5340, 5410, 5476, 5543]*u.AA
52+
line_list = QTable([pixels, wavelength], names=["pixel_center", "wavelength"])
53+
test_cal = WavelengthCalibration1D(lamp_spectrum, matched_line_list=line_list)

specreduce/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,4 @@
77
# ----------------------------------------------------------------------------
88

99
from specreduce.core import * # noqa
10+
from specreduce.wavelength_calibration import * # noqa

specreduce/conftest.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@
66
import os
77

88
from astropy.version import version as astropy_version
9+
import astropy.units as u
10+
import numpy as np
11+
import pytest
12+
from specutils import Spectrum1D
913

1014
# For Astropy 3.0 and later, we can use the standalone pytest plugin
1115
if astropy_version < '3.0':
@@ -20,6 +24,37 @@
2024
ASTROPY_HEADER = False
2125

2226

27+
@pytest.fixture
28+
def spec1d():
29+
np.random.seed(7)
30+
flux = np.random.random(50)*u.Jy
31+
sa = np.arange(0, 50)*u.pix
32+
spec = Spectrum1D(flux, spectral_axis=sa)
33+
return spec
34+
35+
36+
@pytest.fixture
37+
def spec1d_with_emission_line():
38+
np.random.seed(7)
39+
sa = np.arange(0, 200)*u.pix
40+
flux = (np.random.randn(200) +
41+
10*np.exp(-0.01*((sa.value-130)**2)) +
42+
sa.value/100) * u.Jy
43+
spec = Spectrum1D(flux, spectral_axis=sa)
44+
return spec
45+
46+
47+
@pytest.fixture
48+
def spec1d_with_absorption_line():
49+
np.random.seed(7)
50+
sa = np.arange(0, 200)*u.pix
51+
flux = (np.random.randn(200) -
52+
10*np.exp(-0.01*((sa.value-130)**2)) +
53+
sa.value/100) * u.Jy
54+
spec = Spectrum1D(flux, spectral_axis=sa)
55+
return spec
56+
57+
2358
def pytest_configure(config):
2459

2560
if ASTROPY_HEADER:
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
from numpy.testing import assert_allclose
2+
import pytest
3+
4+
from astropy.table import QTable
5+
import astropy.units as u
6+
from astropy.modeling.models import Polynomial1D
7+
from astropy.modeling.fitting import LinearLSQFitter
8+
from astropy.tests.helper import assert_quantity_allclose
9+
10+
from specreduce import WavelengthCalibration1D
11+
12+
13+
def test_linear_from_list(spec1d):
14+
centers = [0, 10, 20, 30]
15+
w = [5000, 5100, 5198, 5305]*u.AA
16+
test = WavelengthCalibration1D(spec1d, line_pixels=centers, line_wavelengths=w)
17+
spec2 = test.apply_to_spectrum(spec1d)
18+
19+
assert_quantity_allclose(spec2.spectral_axis[0], 4998.8*u.AA)
20+
assert_quantity_allclose(spec2.spectral_axis[-1], 5495.169999*u.AA)
21+
22+
23+
def test_wavelength_from_table(spec1d):
24+
centers = [0, 10, 20, 30]
25+
w = [5000, 5100, 5198, 5305]*u.AA
26+
table = QTable([w], names=["wavelength"])
27+
WavelengthCalibration1D(spec1d, line_pixels=centers, line_wavelengths=table)
28+
29+
30+
def test_linear_from_table(spec1d):
31+
centers = [0, 10, 20, 30]
32+
w = [5000, 5100, 5198, 5305]*u.AA
33+
table = QTable([centers, w], names=["pixel_center", "wavelength"])
34+
test = WavelengthCalibration1D(spec1d, matched_line_list=table)
35+
spec2 = test.apply_to_spectrum(spec1d)
36+
37+
assert_quantity_allclose(spec2.spectral_axis[0], 4998.8*u.AA)
38+
assert_quantity_allclose(spec2.spectral_axis[-1], 5495.169999*u.AA)
39+
40+
41+
def test_poly_from_table(spec1d):
42+
# This test is mostly to prove that you can use other models
43+
centers = [0, 10, 20, 30, 40]
44+
w = [5005, 5110, 5214, 5330, 5438]*u.AA
45+
table = QTable([centers, w], names=["pixel_center", "wavelength"])
46+
47+
test = WavelengthCalibration1D(spec1d, matched_line_list=table,
48+
model=Polynomial1D(2), fitter=LinearLSQFitter())
49+
test.apply_to_spectrum(spec1d)
50+
51+
assert_allclose(test.model.parameters, [5.00477143e+03, 1.03457143e+01, 1.28571429e-02])
52+
53+
54+
def test_replace_spectrum(spec1d, spec1d_with_emission_line):
55+
centers = [0, 10, 20, 30]*u.pix
56+
w = [5000, 5100, 5198, 5305]*u.AA
57+
test = WavelengthCalibration1D(spec1d, line_pixels=centers, line_wavelengths=w)
58+
# Accessing this property causes fits the model and caches the resulting WCS
59+
test.wcs
60+
assert "wcs" in test.__dict__
61+
62+
# Replace the input spectrum, which should clear the cached properties
63+
test.input_spectrum = spec1d_with_emission_line
64+
assert "wcs" not in test.__dict__
65+
66+
67+
def test_expected_errors(spec1d):
68+
centers = [0, 10, 20, 30, 40]
69+
w = [5005, 5110, 5214, 5330, 5438]*u.AA
70+
table = QTable([centers, w], names=["pixel_center", "wavelength"])
71+
72+
with pytest.raises(ValueError, match="Cannot specify line_wavelengths separately"):
73+
WavelengthCalibration1D(spec1d, matched_line_list=table, line_wavelengths=w)
74+
75+
with pytest.raises(ValueError, match="must have the same length"):
76+
w2 = [5005, 5110, 5214, 5330, 5438, 5500]*u.AA
77+
WavelengthCalibration1D(spec1d, line_pixels=centers, line_wavelengths=w2)
78+
79+
with pytest.raises(ValueError, match="astropy.units.Quantity array or"
80+
" as an astropy.table.QTable"):
81+
w2 = [5005, 5110, 5214, 5330, 5438]
82+
WavelengthCalibration1D(spec1d, line_pixels=centers, line_wavelengths=w2)
83+
84+
with pytest.raises(ValueError, match="specify at least one"):
85+
WavelengthCalibration1D(spec1d, line_pixels=centers)

0 commit comments

Comments
 (0)