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

Fix with_spectral_unit #1119

Merged
merged 15 commits into from
Feb 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
13 changes: 8 additions & 5 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ Bug Fixes
- ``template_correlate`` no longer errors when used on a ``Spectrum1D`` that lacks an
``uncertainty`` array. [#1118]

- ``with_spectral_unit`` has been changed to ``with_spectral_axis_unit`` and actually works
now. [#1119]

Other Changes and Additions
^^^^^^^^^^^^^^^^^^^^^^^^^^^

Expand Down Expand Up @@ -64,7 +67,7 @@ New Features

- ``wcs1d-fits`` loader now reads and writes celestial components of
of multi-dimensional WCS, and handles ``mask`` and ``uncertainty``
attributes. [#1009]
attributes. [#1009]

- Added support for reading from files with flux in counts. [#1018]

Expand Down Expand Up @@ -183,7 +186,7 @@ Bug Fixes
``Spectrum1D`` without WCS nor spectral axis and the spatial-spatial dimension
is smaller than spectral dimension. [#926]

- Fixed WCS not accurately reflecting the updated spectral axis after slicing a
- Fixed WCS not accurately reflecting the updated spectral axis after slicing a
``Spectrum1D``. [#918]

Other Changes and Additions
Expand Down Expand Up @@ -212,8 +215,8 @@ New Features
- Convolution-based smoothing will now apply a 1D kernel to multi-dimensional fluxes
by convolving along the spectral axis only, rather than raising an error. [#885]

- ``template_comparison`` now handles ``astropy.nddata.Variance`` and
``astropy.nddata.InverseVariance`` uncertainties instead of assuming
- ``template_comparison`` now handles ``astropy.nddata.Variance`` and
``astropy.nddata.InverseVariance`` uncertainties instead of assuming
the uncertainty is standard deviation. [#899]

Bug Fixes
Expand Down Expand Up @@ -253,7 +256,7 @@ New Features

- Allow overriding existing astropy registry elements. [#861]

- ``Spectrum1D`` will now swap the spectral axis with the last axis on initialization
- ``Spectrum1D`` will now swap the spectral axis with the last axis on initialization
if it can be identified from the WCS and is not last, rather than erroring. [#654, #822]

Bug Fixes
Expand Down
2 changes: 1 addition & 1 deletion specutils/analysis/moment.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ def moment(spectrum, regions=None, order=0, axis=-1):
spectrum : `~specutils.spectra.spectrum1d.Spectrum1D`
The spectrum object over which the width will be calculated.

regions: `~specutils.utils.SpectralRegion` or list of `~specutils.utils.SpectralRegion`
regions: `~specutils.SpectralRegion` or list of `~specutils.SpectralRegion`
dhomeier marked this conversation as resolved.
Show resolved Hide resolved
Region within the spectrum to calculate the gaussian sigma width. If
regions is `None`, computation is performed over entire spectrum.

Expand Down
8 changes: 4 additions & 4 deletions specutils/analysis/uncertainty.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ def snr(spectrum, region=None):
spectrum : `~specutils.spectra.spectrum1d.Spectrum1D`
The spectrum object overwhich the equivalent width will be calculated.

region: `~specutils.utils.SpectralRegion` or list of `~specutils.utils.SpectralRegion`
region: `~specutils.SpectralRegion` or list of `~specutils.SpectralRegion`
dhomeier marked this conversation as resolved.
Show resolved Hide resolved
Region within the spectrum to calculate the SNR.

Returns
Expand Down Expand Up @@ -67,7 +67,7 @@ def _snr_single_region(spectrum, region=None):
spectrum : `~specutils.spectra.spectrum1d.Spectrum1D`
The spectrum object overwhich the equivalent width will be calculated.

region: `~specutils.utils.SpectralRegion`
region: `~specutils.SpectralRegion`
dhomeier marked this conversation as resolved.
Show resolved Hide resolved
Region within the spectrum to calculate the SNR.

Returns
Expand Down Expand Up @@ -109,7 +109,7 @@ def snr_derived(spectrum, region=None):
spectrum : `~specutils.spectra.spectrum1d.Spectrum1D`
The spectrum object overwhich the equivalent width will be calculated.

region: `~specutils.utils.SpectralRegion`
region: `~specutils.SpectralRegion`
dhomeier marked this conversation as resolved.
Show resolved Hide resolved
Region within the spectrum to calculate the SNR.

Returns
Expand Down Expand Up @@ -155,7 +155,7 @@ def _snr_derived(spectrum, region=None):
spectrum : `~specutils.spectra.spectrum1d.Spectrum1D`
The spectrum object overwhich the equivalent width will be calculated.

region: `~specutils.utils.SpectralRegion`
region: `~specutils.SpectralRegion`
dhomeier marked this conversation as resolved.
Show resolved Hide resolved
Region within the spectrum to calculate the SNR.

Returns
Expand Down
85 changes: 30 additions & 55 deletions specutils/spectra/spectrum_mixin.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@
from astropy import units as u
from astropy.utils.decorators import deprecated

from specutils.utils.wcs_utils import gwcs_from_array

DOPPLER_CONVENTIONS = {}
DOPPLER_CONVENTIONS['radio'] = u.doppler_radio
DOPPLER_CONVENTIONS['optical'] = u.doppler_optical
Expand Down Expand Up @@ -184,10 +182,20 @@

return new_data

@deprecated('v1.13', alternative="with_spectral_axis_unit")
def with_spectral_unit(self, unit, velocity_convention=None,
rest_value=None):
self.with_spectral_axis_unit(unit, velocity_convention=velocity_convention,

Check warning on line 188 in specutils/spectra/spectrum_mixin.py

View check run for this annotation

Codecov / codecov/patch

specutils/spectra/spectrum_mixin.py#L188

Added line #L188 was not covered by tests
rest_value=rest_value)

def with_spectral_axis_unit(self, unit, velocity_convention=None,
rest_value=None):
"""
Returns a new spectrum with a different spectral axis unit.
Returns a new spectrum with a different spectral axis unit. Note that this creates a new
object using the converted spectral axis and thus drops the original WCS, if it existed,
replacing it with a lookup-table :class:`~gwcs.wcs.WCS` based on the new spectral axis. The
original WCS will be stored in the ``original_wcs`` entry of the new object's ``meta``
dictionary.

Parameters
----------
Expand All @@ -208,14 +216,27 @@
even if your spectrum has air wavelength units

"""
new_wcs, new_meta = self._new_spectral_wcs(
unit=unit,
velocity_convention=velocity_convention or self.velocity_convention,
rest_value=rest_value or self.rest_value)

spectrum = self.__class__(flux=self.flux, wcs=new_wcs, meta=new_meta)
velocity_convention = velocity_convention if velocity_convention is not None else self.velocity_convention # noqa
rest_value = rest_value if rest_value is not None else self.rest_value
unit = self._new_wcs_argument_validation(unit, velocity_convention,
rest_value)

# Store the original unit information and WCS for posterity
meta = deepcopy(self._meta)

if 'original_spectral_axis_unit' not in self._meta:
orig_unit = self.wcs.unit[0] if hasattr(self.wcs, 'unit') else self.spectral_axis.unit
meta['original_spectral_axis_unit'] = orig_unit

if 'original_wcs' not in self.meta:
meta['original_wcs'] = self.wcs.deepcopy()

return spectrum
new_spectral_axis = self.spectral_axis.to(unit, doppler_convention=velocity_convention,
doppler_rest=rest_value)

return self.__class__(flux=self.flux, spectral_axis=new_spectral_axis, meta=meta,
uncertainty=self.uncertainty, mask=self.mask)

def _new_wcs_argument_validation(self, unit, velocity_convention,
rest_value):
Expand All @@ -242,52 +263,6 @@

return unit

def _new_spectral_wcs(self, unit, velocity_convention=None,
rest_value=None):
"""
Returns a new WCS with a different Spectral Axis unit.

Parameters
----------
unit : :class:`~astropy.units.Unit`
Any valid spectral unit: velocity, (wave)length, or frequency.
Only vacuum units are supported.
velocity_convention : 'relativistic', 'radio', or 'optical'
The velocity convention to use for the output velocity axis.
Required if the output type is velocity. This can be either one
of the above strings, or an `astropy.units` equivalency.
rest_value : :class:`~astropy.units.Quantity`
A rest wavelength or frequency with appropriate units. Required if
output type is velocity. The cube's WCS should include this
already if the *input* type is velocity, but the WCS's rest
wavelength/frequency can be overridden with this parameter.

.. note: This must be the rest frequency/wavelength *in vacuum*,
even if your cube has air wavelength units
"""

unit = self._new_wcs_argument_validation(unit, velocity_convention,
rest_value)

if velocity_convention is not None:
equiv = getattr(u, 'doppler_{0}'.format(velocity_convention))
rest_value.to(unit, equivalencies=equiv)

# Store the original unit information for posterity
meta = self._meta.copy()

orig_unit = self.wcs.unit[0] if hasattr(self.wcs, 'unit') else self.spectral_axis.unit

if 'original_unit' not in self._meta:
meta['original_unit'] = orig_unit

# Create the new wcs object
if isinstance(unit, u.UnitBase) and unit.is_equivalent(
orig_unit, equivalencies=u.spectral()):
return gwcs_from_array(self.spectral_axis), meta

raise u.UnitConversionError(f"WCS units incompatible: {unit} and {orig_unit}.")

def _check_strictly_increasing_decreasing(self):
"""
Check that the self._spectral_axis is strictly increasing or decreasing
Expand Down
23 changes: 21 additions & 2 deletions specutils/tests/test_spectrum1d.py
Original file line number Diff line number Diff line change
Expand Up @@ -150,10 +150,29 @@ def test_spectral_axis_conversions():
with pytest.raises(ValueError):
spec.velocity

spec = Spectrum1D(spectral_axis=np.arange(1, 50) * u.nm,
spec = Spectrum1D(spectral_axis=np.arange(100, 150) * u.nm,
flux=np.random.randn(49) * u.Jy)

new_spec = spec.with_spectral_unit(u.GHz) # noqa
new_spec = spec.with_spectral_axis_unit(u.km/u.s, rest_value=125*u.um,
velocity_convention="relativistic")

assert new_spec.spectral_axis.unit == u.km/u.s
assert new_spec.wcs.world_axis_units[0] == "km.s**-1"
# Make sure meta stored the old WCS correctly
assert new_spec.meta["original_wcs"].world_axis_units[0] == "nm"
assert new_spec.meta["original_spectral_axis_unit"] == "nm"
rosteen marked this conversation as resolved.
Show resolved Hide resolved

wcs_dict = {"CTYPE1": "WAVE", "CRVAL1": 3.622e3, "CDELT1": 8e-2,
"CRPIX1": 0, "CUNIT1": "Angstrom"}
wcs_spec = Spectrum1D(flux=np.random.randn(49) * u.Jy, wcs=WCS(wcs_dict),
meta={'header': wcs_dict.copy()})
new_spec = wcs_spec.with_spectral_axis_unit(u.km/u.s, rest_value=125*u.um,
velocity_convention="relativistic")
new_spec.meta['original_wcs'].wcs.crval = [3.777e-7]
new_spec.meta['header']['CRVAL1'] = 3777.0

assert wcs_spec.wcs.wcs.crval[0] == 3.622e-7
assert wcs_spec.meta['header']['CRVAL1'] == 3622.


def test_spectral_slice():
Expand Down
48 changes: 40 additions & 8 deletions specutils/utils/wcs_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,44 @@
from gwcs import coordinate_frames as cf


class SpectralGWCS(GWCS):
"""
This is a placeholder lookup-table GWCS created when a :class:`~specutils.Spectrum1D` is
instantiated with a ``spectral_axis`` and no WCS.
"""
def __init__(self, *args, **kwargs):
self.original_unit = kwargs.pop("original_unit", "")
super().__init__(*args, **kwargs)

def copy(self):
"""
Return a shallow copy of the object.

Convenience method so user doesn't have to import the
:mod:`copy` stdlib module.

.. warning::
Use `deepcopy` instead of `copy` unless you know why you need a
shallow copy.
"""
return copy.copy(self)

Check warning on line 31 in specutils/utils/wcs_utils.py

View check run for this annotation

Codecov / codecov/patch

specutils/utils/wcs_utils.py#L31

Added line #L31 was not covered by tests

def deepcopy(self):
"""
Return a deep copy of the object.

Convenience method so user doesn't have to import the
:mod:`copy` stdlib module.
"""
return copy.deepcopy(self)

def pixel_to_world(self, *args, **kwargs):
if self.original_unit == '':
return u.Quantity(super().pixel_to_world_values(*args, **kwargs))
return super().pixel_to_world(*args, **kwargs).to(
self.original_unit, equivalencies=u.spectral())


Copy link
Contributor

Choose a reason for hiding this comment

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

Perhaps just copy [pun-trigger!] over
https://github.com/astropy/astropy/blob/97bf1e123feec3170e2244964b0df2852b8e54e1/astropy/wcs/wcs.py#L633-L644
ff here as well for compatibility and to fix CI failure?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Weird, I thought I ran the tests locally after adding that, guess not. Thanks for the link.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Actually the tests do pass locally, I must have the IERS files cached.

def refraction_index(wavelength, method='Griesen2006', co2=None):
"""
Calculates the index of refraction of dry air at standard temperature
Expand Down Expand Up @@ -210,14 +248,8 @@
forward_transform.inverse = SpectralTabular1D(
array[::-1], lookup_table=np.arange(len(array))[::-1])

class SpectralGWCS(GWCS):
def pixel_to_world(self, *args, **kwargs):
if orig_array.unit == '':
return u.Quantity(super().pixel_to_world_values(*args, **kwargs))
return super().pixel_to_world(*args, **kwargs).to(
orig_array.unit, equivalencies=u.spectral())

tabular_gwcs = SpectralGWCS(forward_transform=forward_transform,
tabular_gwcs = SpectralGWCS(original_unit = orig_array.unit,
forward_transform=forward_transform,
input_frame=coord_frame,
output_frame=spec_frame)

Expand Down
Loading