Skip to content

Add SpectralCoord class to astropy.coordinates#10185

Merged
eteq merged 155 commits intoastropy:masterfrom
astrofrog:add-spectralcoord
May 2, 2020
Merged

Add SpectralCoord class to astropy.coordinates#10185
eteq merged 155 commits intoastropy:masterfrom
astrofrog:add-spectralcoord

Conversation

@astrofrog
Copy link
Member

@astrofrog astrofrog commented Apr 22, 2020

About

This pull request implements a new class, SpectralCoord which aims to represent spectral coordinates (wavelengths, frequencies, energies, velocities) and also encapsulates information about observer and target. This starts from the implementation @nmearl wrote in specutils. Also implemented here is a SpectralQuantity class with default spectral equivalencies as well as a mechanism for keeping track of the doppler convention and rest value.

The motivation behind this class was:

  • to provide a high-level object for use in APE 14 spectral WCSes when calling WCS.pixel_to_world - currently we returned a Quantity with no meta-data, whereas now we can let the user know what frame of reference the spectral coordinate is for, the direction of the target, and Doppler rest frequencies/wavelengths and conventions used for spectral axes in velocity (this is all information that is part of the FITS WCS standard)

  • to provide a class that could be used in specutils to represent spectral axes (in practice specutils will subclass SpectralCoord into SpectralAxis to provide axis-specific functionality too)

Developers are welcome to start reviewing this PR, but note that there are a few remaining to-dos listed lower down, so this is not at the 'final draft' stage. If you have limited time, you could consider just reviewing the docs to see if you are happy with the API.

Changes to astropy.coordinates

The first set of changes here is to astropy.coordinates - we add a new class SpectralCoord which is a quantity sub-class with metadata and transformation methods. This also includes a new narrative docs page to describe it. This documentation page is not complete in the sense that some of the functionality is not described, but this provides a starting point, and what's there currently is ready for review.

Note that SpectralCoord is deliberately a Quantity sub-class rather than using a more complex infrastructure like SkyCoord with many layers of classes. Since we only care about a single coordinate here, this was found to be the simplest API, and also provides backward-compatibility with the current use of the APE 14 FITS WCS (which previously returned Quantity). We do use coordinate frames/SkyCoord to represent observers and targets though.

I have included a test that does detailed numerical checks against SLALIB - I plan to include more details about how this was done. For a reason I don't understand our galactocentric results are different, and I will be delving into that. For other frames, we typically get ~20m/s agreement for now.

Changes to astropy.units

I've added a SpectralQuantity class which implements a Quantity subclass that automatically includes spectral equivalencies, and keeps track of the doppler convention and rest value.

Changes to astropy.wcs.wcsapi

The second set of changes is to the APE 14 FITS WCS implementation - here we now return SpectralCoord for pixel_to_world - note that we stilll accept Quantity as input for world_to_pixel but in this case we give a warning that there is no observer defined, so no velocity transformation will be carried out.

One big caveat is that for now, the target on the SpectralCoord is set to the reference celestial coordinate in the case of spectral cubes. For cubes with large fields of view, this won't be quite right away from the reference coordinate. But this is already better than the current situation of returning Quantity. So I am planning to add a caveat about this to the APE 14 WCS docs. Properly implementing this may be tricky because it requires interpreting the SSYSOBS keyword which many cubes lack, and in principle this means that even for an LSRK cube the spectral coordinate is actually intended to be constant across slices in the topocentric frame, which means the spectral axis is correlated with the celestial axes. I think this is a little known fact and many cubes may be produced not knowing this assumption, which I think is why the current implementation may actually reflect the way most people use cubes (and pragmatically, it's also a much easier starting point).

git history

The git history was extracted from specutils (where an initial implementation of this class was added), and I've taken care to preserve the PR merge history so that we have references to the original PRs. I can also remove these merge commits if people would prefer, but I think it's quite nice to be able to see the original chunks of work that were done. I'm happy to go with majority opinion!

Remaining to-dos

  • Make sure the sign of the transformations is correct
  • Include code to generate reference rv file, and provide details of where this comes from
  • Include ASDF schema and tag
  • Fix tests
  • Add changelog entry
  • Add test for ASDF serialization
  • Try and make some improvements to API based on user feedback
  • Write documentation for astropy.wcs changes
  • Fix xfailed ASDF serialization tests
  • Write what's new entry
  • Clean up implementation

Things that will be done later after this PR is merged

nmearl and others added 30 commits September 24, 2019 10:06
@astrofrog
Copy link
Member Author

@mhvk - just to check, are you now happy with this as-is, or are you planning on doing another round of review?

(I believe @eteq is currently reviewing this, so this should not be merged yet)

@mhvk
Copy link
Contributor

mhvk commented May 1, 2020

Am hoping to have another look... But OK to merge if that doesn't happen before end of day.

Copy link
Member

@eteq eteq left a comment

Choose a reason for hiding this comment

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

Alright, I've finished a full review - it's almost all minor suggestion/nitpicks, and I'm approving because none are blockers. A few higher-level items that didn't have an obvious inline-location:

  • Should SpectralQuantity be in units instead of coordinates?
  • Somewhere in the docs this should say that it's the relativistic redshift to rv convention, not v=c*z . That is a convention, even if it's not the most "physical" one.

And two more items that could be follow-on items (i.e., could be done now or later depending on time left):

  • Is a "quick start"/at-a-glance section needed for the SpectralCoord docs?
  • SpectrumQuantity would be quite a bit more convenient if it had the spectral flux density equivalency as a semi-baked in option. To not delay anything I'll just try implementing that as a PR to this PR, but can always change it to against master and include it later.

elif doppler_convention not in DOPPLER_CONVENTIONS:
raise ValueError("doppler_convention should be one of {0}".format('/'.join(sorted(DOPPLER_CONVENTIONS))))

if self.unit.is_equivalent(si.km / si.s) and unit.is_equivalent(si.km / si.s):
Copy link
Member

Choose a reason for hiding this comment

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

minor (premature?) optimization you can take or ignore as you like: have a si.km/si.s computed at the global level do not have to make km/s every time to is called

Copy link
Member Author

Choose a reason for hiding this comment

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

Done

radial_velocity : `~astropy.units.Quantity`, optional
The radial velocity of the target with respect to the observer.
redshift : float, optional
The relativistic redshift of the target with respect to the observer.
Copy link
Member

Choose a reason for hiding this comment

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

Might be worth noting you can't give RV and redshift at the same time?

Copy link
Member Author

Choose a reason for hiding this comment

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

Done

return self.__class__(**default_kwargs)

@property
def quantity(self):
Copy link
Member

@eteq eteq May 1, 2020

Choose a reason for hiding this comment

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

I preferred as_quantity, but maybe this ship has sailed... (couldn't find @mhvk's logic...)

EDIT by MHvK: It seems I cannot answer, so editing: the logic was to be similar to .value and .physical (from magnitudes, etc.), and also .si, .cgs, .degree (for angle).

Co-authored-by: Erik Tollerud <erik.tollerud@gmail.com>
@mhvk
Copy link
Contributor

mhvk commented May 1, 2020

@astrofrog - I don't think I'll get around to another round, but as others approved (and it looked very good), I'm fine with it being merged when you're done with implementing comments and CI passes, etc.

@astrofrog
Copy link
Member Author

astrofrog commented May 1, 2020

@eteq - thanks for the review! A few quick notes:

Should SpectralQuantity be in units instead of coordinates?

It moved there temporarily during this PR, but it was suggested that it should be moved back, as other classes such as Distance and so on also live there.

Somewhere in the docs this should say that it's the relativistic redshift to rv convention, not v=c*z . That is a convention, even if it's not the most "physical" one.

I've added a note to the docs in the latest commit: 28197eb

Is a "quick start"/at-a-glance section needed for the SpectralCoord docs?

I don't think it would be easy to add one in the spectralcoord.rst page since I do think we need to explain things step by step, but I'm opening to someone giving it a try!

SpectrumQuantity would be quite a bit more convenient if it had the spectral flux density equivalency as a semi-baked in option. To not delay anything I'll just try implementing that as a PR to this PR, but can always change it to against master and include it later.

I'll check your PR - thanks!

@mhvk
Copy link
Contributor

mhvk commented May 1, 2020

@eteq, @astrofrog - I'm not sure I understand the use for spectral flux density - would this be if combined with a Quantity of flux densities? If it is not totally obvious, I'd go for another PR - this one is very long already (which is mostly why I haven't been able to get back to it).

@astrofrog
Copy link
Member Author

@mhvk - here is @eteq's PR: astrofrog#100 - it's more a convenience property to get a spectral density equivalency for that spectral quantity.

@mhvk
Copy link
Contributor

mhvk commented May 1, 2020

Ah, that property makes sense, though personally I'd first see what is useful in practice - after all, fd.to(unit, sq.spectral_density()) is not much less typing than fd.to(unit, u.spectral_density(sq)).

@eteq
Copy link
Member

eteq commented May 2, 2020

Ah, that property makes sense, though personally I'd first see what is useful in practice - after all, fd.to(unit, sq.spectral_density()) is not much less typing than fd.to(unit, u.spectral_density(sq)).

OK, I'll re-cast that into a PR against master since I don't want it to block this one. Since it's clearly had lots of great review at this point and everything as passing, I'm merging this now. Thanks everyone for all the great work on this!

@eteq eteq merged commit 8b55e6d into astropy:master May 2, 2020
pllim added a commit to pllim/astropy that referenced this pull request May 2, 2020
@bsipocz
Copy link
Member

bsipocz commented May 2, 2020

is specutils compatible with this one, or it's not in the plan to switch to using this straight away? (e.g. how much coordination should the release here wait, etc?)

@pllim
Copy link
Member

pllim commented May 2, 2020

Is there a need to also document this in https://docs.astropy.org/en/latest/io/misc.html#schemas ?

@Cadair
Copy link
Member

Cadair commented May 2, 2020

So this broke SunPy's astropy-dev build FYI. I will investigate.

@taldcroft
Copy link
Member

This broke #10154, though one might reasonably view things the other way round. I had to change a spectral coord test due to the strict definition of equality now: 3082a81.

I do have to wonder if #10154 will break other tests of code that were explicitly or implicitly using == as a substitute for is. The first version of #10154 returned False instead of raising when a comparison cannot be done, but based on the way things are going in numpy, we decided that comparing objects that can't be compared should raise an exception.

@mhvk
Copy link
Contributor

mhvk commented May 2, 2020

Perhaps we should reconsider raising the exception and instead return False after all? That said, I think the test was not really clean either. And there is a case to be made for making clear to the user why a comparison cannot be done. Anyway, obviously numpy struggled with the same...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.