Skip to content
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

Implement 1D wavelength calibration classes #162

Merged
merged 31 commits into from
Apr 12, 2023
Merged
Show file tree
Hide file tree
Changes from 30 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
d58f652
Working on line pixel refinement
rosteen Jan 18, 2023
1f3483a
Testing/debugging CalibrationLine, starting on WavelengthCalibration
rosteen Jan 19, 2023
c54aed7
Working on model fitting and construction of resulting GWCS
rosteen Jan 26, 2023
5eb4aaa
Working through bugs/testing
rosteen Jan 26, 2023
baa706c
Working initial implementation
rosteen Jan 26, 2023
02ed67e
Codestyle
rosteen Jan 26, 2023
1df8621
Match order of pixel/wavelength between CalibrationLine and Wavelengt…
rosteen Jan 27, 2023
eb5feea
Fix codestyle in test
rosteen Jan 27, 2023
6ea98e4
Adding tests for CalibrationLine
rosteen Jan 31, 2023
af4a523
Fix codestlye
rosteen Jan 31, 2023
568fd09
Increase atol for gaussian center
rosteen Jan 31, 2023
be35c09
Remove autoidentify for now, add Polynomial1D test
rosteen Feb 1, 2023
f3b1b9f
Add cache clearing, avoid AstropyUserWarning
rosteen Feb 1, 2023
8117864
Add input_spectrum property and clear cache on setting it
rosteen Feb 1, 2023
8f1535b
Add default range if method is set but not range, remove check for un…
rosteen Feb 2, 2023
22e2d68
Also clear cache when updating lines or model
rosteen Feb 3, 2023
4a508e3
Move fitter default to wcs call
rosteen Feb 3, 2023
a8ca23f
Keep storing fitter as None
rosteen Feb 3, 2023
1941a26
Add docs, improve importing of new classes
rosteen Feb 7, 2023
829dc5c
Remove CalibrationLine, move to astropy tables internallyt
rosteen Apr 6, 2023
c3ba30d
Codestyle
rosteen Apr 6, 2023
62c8025
Apply suggestions from code review
rosteen Apr 7, 2023
12a8661
Remove speactral_units arg, add docstring for fitter arg
rosteen Apr 7, 2023
5063b5e
Update documentation, add NotImplementedError for catalog input
rosteen Apr 7, 2023
b6a92f8
Reverse sort frequencies
rosteen Apr 7, 2023
1aa62c6
Codestyle
rosteen Apr 7, 2023
7f31b61
Increase test coverage
rosteen Apr 10, 2023
45939a9
Fix tests and bugs found by tests
rosteen Apr 10, 2023
f92bac5
Codestyle
rosteen Apr 10, 2023
d573219
Add QTable to catalog options
rosteen Apr 10, 2023
82170a5
Finish addressing review comments
rosteen Apr 11, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,8 @@
{
'astropy': ('https://docs.astropy.org/en/stable/', None),
'ccdproc': ('https://ccdproc.readthedocs.io/en/stable/', None),
'specutils': ('https://specutils.readthedocs.io/en/stable/', None)
'specutils': ('https://specutils.readthedocs.io/en/stable/', None),
'gwcs': ('https://gwcs.readthedocs.io/en/stable/', None)
}
)
#
Expand Down
1 change: 1 addition & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ Calibration
.. toctree::
:maxdepth: 1

wavelength_calibration.rst
extinction.rst
specphot_standards.rst

Expand Down
52 changes: 52 additions & 0 deletions docs/wavelength_calibration.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
.. _wavelength_calibration:

Wavelength Calibration
======================

Wavelength calibration is currently supported for 1D spectra. Given a list of spectral
lines with known wavelengths and estimated pixel positions on an input calibration
spectrum, you can currently use ``specreduce`` to:

#. Fit an ``astropy`` model to the wavelength/pixel pairs to generate a spectral WCS
solution for the dispersion.
#. Apply the generated spectral WCS to other `~specutils.Spectrum1D` objects.

1D Wavelength Calibration
-------------------------

The `~specreduce.wavelength_calibration.WavelengthCalibration1D` class can be used
to fit a dispersion model to a list of line positions and wavelengths. Future development
will implement catalogs of known lamp spectra for use in matching observed lines. In the
example below, the line positions (``line_list``) have already been extracted from
``lamp_spectrum``::

import astropy.units as u
from specreduce import WavelengthCalibration1D
line_list = [10, 22, 31, 43]
wavelengths = [5340, 5410, 5476, 5543]*u.AA
test_cal = WavelengthCalibration1D(lamp_spectrum, line_list,
line_wavelengths=wavelengths)
calibrated_spectrum = test_cal.apply_to_spectrum(science_spectrum)

The example above uses the default model (`~astropy.modeling.functional_models.Linear1D`)
to fit the input spectral lines, and then applies the calculated WCS solution to a second
spectrum (``science_spectrum``). Any other ``astropy`` model can be provided as the
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe specify "any 1D model"

input ``model`` parameter to the `~specreduce.wavelength_calibration.WavelengthCalibration1D`.
In the above example, the model fit and WCS construction is all done as part of the
``apply_to_spectrum()`` call, but you could also access the `~gwcs.wcs.WCS` object itself
by calling::

test_cal.wcs

The calculated WCS is a cached property that will be cleared if the ``line_list``, ``model``,
or ``input_spectrum`` properties are updated, since these will alter the calculated dispersion
fit.

You can also provide the input pixel locations and wavelengths of the lines as an
`~astropy.table.QTable`, with columns ``pixel_center`` and ``wavelength``::

from astropy.table import QTable
pixels = [10, 20, 30, 40]*u.pix
wavelength = [5340, 5410, 5476, 5543]*u.AA
line_list = QTable([pixels, wavelength], names=["pixel_center", "wavelength"])
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpick: i'd call this matched_line_list so as not to be confused with linelist or line_list which is usually understood to mean something else (i.e. list of wavelengths of lines due to specific ion/molecule or mixture of of them).

test_cal = WavelengthCalibration1D(lamp_spectrum, line_list)
1 change: 1 addition & 0 deletions specreduce/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@
# ----------------------------------------------------------------------------

from specreduce.core import * # noqa
from specreduce.wavelength_calibration import * # noqa
35 changes: 35 additions & 0 deletions specreduce/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@
import os

from astropy.version import version as astropy_version
import astropy.units as u
import numpy as np
import pytest
from specutils import Spectrum1D

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


@pytest.fixture
def spec1d():
np.random.seed(7)
flux = np.random.random(50)*u.Jy
sa = np.arange(0, 50)*u.pix
spec = Spectrum1D(flux, spectral_axis=sa)
return spec


@pytest.fixture
def spec1d_with_emission_line():
np.random.seed(7)
sa = np.arange(0, 200)*u.pix
flux = (np.random.randn(200) +
10*np.exp(-0.01*((sa.value-130)**2)) +
sa.value/100) * u.Jy
spec = Spectrum1D(flux, spectral_axis=sa)
return spec


@pytest.fixture
def spec1d_with_absorption_line():
np.random.seed(7)
sa = np.arange(0, 200)*u.pix
flux = (np.random.randn(200) -
10*np.exp(-0.01*((sa.value-130)**2)) +
sa.value/100) * u.Jy
spec = Spectrum1D(flux, spectral_axis=sa)
return spec


Comment on lines +27 to +57
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

would this benefit from using the synthetic spectra from #165?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, but I think using that to expand/improve the tests can be another PR once both of these are merged.

def pytest_configure(config):

if ASTROPY_HEADER:
Expand Down
84 changes: 84 additions & 0 deletions specreduce/tests/test_wavelength_calibration.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
from numpy.testing import assert_allclose
import pytest

from astropy.table import QTable
import astropy.units as u
from astropy.modeling.models import Polynomial1D
from astropy.modeling.fitting import LinearLSQFitter
from astropy.tests.helper import assert_quantity_allclose

from specreduce import WavelengthCalibration1D


def test_linear_from_list(spec1d):
centers = [0, 10, 20, 30]
w = [5000, 5100, 5198, 5305]*u.AA
test = WavelengthCalibration1D(spec1d, centers, line_wavelengths=w)
spec2 = test.apply_to_spectrum(spec1d)

assert_quantity_allclose(spec2.spectral_axis[0], 4998.8*u.AA)
assert_quantity_allclose(spec2.spectral_axis[-1], 5495.169999*u.AA)


def test_wavelength_from_table(spec1d):
centers = [0, 10, 20, 30]
w = [5000, 5100, 5198, 5305]*u.AA
table = QTable([w], names=["wavelength"])
WavelengthCalibration1D(spec1d, centers, line_wavelengths=table)


def test_linear_from_table(spec1d):
centers = [0, 10, 20, 30]
w = [5000, 5100, 5198, 5305]*u.AA
table = QTable([centers, w], names=["pixel_center", "wavelength"])
test = WavelengthCalibration1D(spec1d, table)
spec2 = test.apply_to_spectrum(spec1d)

assert_quantity_allclose(spec2.spectral_axis[0], 4998.8*u.AA)
assert_quantity_allclose(spec2.spectral_axis[-1], 5495.169999*u.AA)


def test_poly_from_table(spec1d):
# This test is mostly to prove that you can use other models
centers = [0, 10, 20, 30, 40]
w = [5005, 5110, 5214, 5330, 5438]*u.AA
table = QTable([centers, w], names=["pixel_center", "wavelength"])

test = WavelengthCalibration1D(spec1d, table, model=Polynomial1D(2), fitter=LinearLSQFitter())
test.apply_to_spectrum(spec1d)

assert_allclose(test.model.parameters, [5.00477143e+03, 1.03457143e+01, 1.28571429e-02])


def test_replace_spectrum(spec1d, spec1d_with_emission_line):
centers = [0, 10, 20, 30]*u.pix
w = [5000, 5100, 5198, 5305]*u.AA
test = WavelengthCalibration1D(spec1d, centers, line_wavelengths=w)
# Accessing this property causes fits the model and caches the resulting WCS
test.wcs
assert "wcs" in test.__dict__

# Replace the input spectrum, which should clear the cached properties
test.input_spectrum = spec1d_with_emission_line
assert "wcs" not in test.__dict__


def test_expected_errors(spec1d):
centers = [0, 10, 20, 30, 40]
w = [5005, 5110, 5214, 5330, 5438]*u.AA
table = QTable([centers, w], names=["pixel_center", "wavelength"])

with pytest.raises(ValueError, match="Cannot specify line_wavelengths separately"):
WavelengthCalibration1D(spec1d, table, line_wavelengths=w)

with pytest.raises(ValueError, match="must have the same length"):
w2 = [5005, 5110, 5214, 5330, 5438, 5500]*u.AA
WavelengthCalibration1D(spec1d, centers, line_wavelengths=w2)

with pytest.raises(ValueError, match="astropy.units.Quantity array or"
" as an astropy.table.QTable"):
w2 = [5005, 5110, 5214, 5330, 5438]
WavelengthCalibration1D(spec1d, centers, line_wavelengths=w2)

with pytest.raises(ValueError, match="specify at least one"):
WavelengthCalibration1D(spec1d, centers)
Loading