Repository navigation
Masked frames and SkyCoord - #17016
Conversation
|
Thank you for your contribution to Astropy! 🌌 This checklist is meant to remind the package maintainers who will review this pull request of some common things to look for.
|
|
👋 Thank you for your draft pull request! Do you know that you can use |
|
I'm taking this out of draft given the upcoming feature-freeze deadline -- if we miss that, masked I think this is complete if somewhat minimal, with the main question whether one should go beyond simply propagating masks (as done here). E.g., what should expected behaviour be for coordinate conversions and methods like My own 2¢ is that we should simply go with the minimal implementation I have, mark coordinate masks as experimental, and allow masks not to propagate in some specific cases later. cc @taldcroft, @StuartLittlefair, @adrn, @eerovaher, @nstarman |
|
I like the look of this a lot, but I'm not sure about the special treatment of spherical coordinates to disregard the distance. A few things: 1. If a user passes a masked distance and unmasked angles I think they would be very surprised to not find that in the total mask. 2. I think if a user wants to mark a distance as truly missing they should use Nan. Which of course can be combined with a mask, but these have different meanings, right? |
Thanks for bringing that up! It is the thing I have most struggled with, what exactly to do with missing distances, as well as incomplete differentials (proper motions and radial velocities). The basic problem is that a For the mask, I hope my
Which one would you prefer? p.s. Part of the reason to have masks is to allow table joins in which one table does not have the given |
I lean towards the 2nd one. In the lower classes |
|
Have you thought about how |
If I'm right, it will "just work", i.e., propagate, but let's add a test! |
|
Test added - it works as expected. |
Just to be sure: still exclude in the mask possible attributes, right, i.e., effectively just return One big advantage of this approach is that it gives a bit more leeway in postponing worrying about how, e.g., Anyway, I can go either way. Would be good to have input from others, as the question of how to deal with different masks for different coordinate parts is really at the heart of it all. |
That's an interesting Q. Have to think on that more. I would say that if we're marking this as an experimental feature then yes, for now. We can revisit the question of attributes after v7.
Agreed, if I'm interpreting this right and you mean figuring out the added arguments can happen post v7. e.g. with mask_propagator(mode="on-sky"):
...But can we decide this for v7.1? |
eerovaher
left a comment
There was a problem hiding this comment.
I lean towards the 2nd one. In the lower classes .mask combines all masks. IMO we should keep that pattern and make sure that other common use cases are deeply supported.
Just to be sure: still exclude in the mask possible attributes, right, i.e., effectively just return self.data.mask (would be consistent with Time).
Consistency is good!
| return out | ||
|
|
||
|
|
||
| class CoordinateSharedMethods: |
There was a problem hiding this comment.
Why do we need this class? Why can't we implement the methods here on BaseCoordinateFrame and make them dynamically available on SkyCoord through __getattr__()? I wouldn't normally suggest code that is this dynamic, but SkyCoord is designed to be like that so we might as well make use of it.
There was a problem hiding this comment.
The problem is that while just letting __getattr__ take care works for masked and mask, it does not work for .get_mask, since it takes a series of attributes of the class and not all SkyCoord attributes are available on the frame (this is explicitly tested with pressure), i.e., self is required to be the SkyCoord, not its underlying frame.
p.s. Of course, having this shared method mixin will also help remove duplication between the two like for .insert().
There was a problem hiding this comment.
p.p.s. A side benefit of having a mixin class is that, eventually, we can actually have sphinx render the relevant properties and methods (once :inherited-members: works properly).
There was a problem hiding this comment.
What's the reasoning of having masked and mask here? Is it just to keep the masking-related code together?
There was a problem hiding this comment.
Yes, just to keep things together, and with a view of expanding later. But if this is something that will prevent this PR from getting into 7.0, I'm fine with removing this update, and just duplicating SkyCoord.get_mask...
There was a problem hiding this comment.
CoordinateSharedMethods looks like a private implementation detail that we should be able to change without notice, but it does implement methods that are clearly a part of the public API. I am very hesitant to introduce such classes to coordinates until the APE about public API has been accepted.
I would implement methods on BaseCoordinateFrame if SkyCoord can use them via __getattr__() because there are several methods that are implemented like that already. For get_mask() it might be best to avoid code duplication by introducing a private _get_mask() that both SkyCoord.get_mask() and BaseCoordinateFrame.get_mask() can call.
There was a problem hiding this comment.
Yes, fair point that really it is best not to mix new types of implementation in. Will make the suggested changes.
| For spherical representations, the mask of the distance is ignored, | ||
| i.e., one gets the combined mask of the longitude and latitude. |
There was a problem hiding this comment.
I don't like this special casing at all. There are very good reasons for providing convenience methods that combine the masks of longitude- and latitude-like coordinates, but those convenience methods belong in the spherical representation classes and not in the base class. Also, I think we can afford implementing them in v7.1 instead of rushing them right now.
There was a problem hiding this comment.
OK on no special casing (see general comments). But I disagree this should be pushed down to the representations - I'd like those to be ignorant of astronomy! Though once we move the option to special-case to get_mask(), I can see pushing an option to just do angles down to SpecialRepresentation.get_mask().
There was a problem hiding this comment.
I agree. Separation of concerns + fine-grain options in get_mask.
mhvk
left a comment
There was a problem hiding this comment.
Thanks, @nstarman and @eerovaher. Looks like both of you are pretty clear you want the standard .mask just OR together all the masks in the representation and differential. That certainly is a valid choice, and simpler, so I'll go ahead and change it. Some smaller replies inline.
| return out | ||
|
|
||
|
|
||
| class CoordinateSharedMethods: |
There was a problem hiding this comment.
The problem is that while just letting __getattr__ take care works for masked and mask, it does not work for .get_mask, since it takes a series of attributes of the class and not all SkyCoord attributes are available on the frame (this is explicitly tested with pressure), i.e., self is required to be the SkyCoord, not its underlying frame.
p.s. Of course, having this shared method mixin will also help remove duplication between the two like for .insert().
| For spherical representations, the mask of the distance is ignored, | ||
| i.e., one gets the combined mask of the longitude and latitude. |
There was a problem hiding this comment.
OK on no special casing (see general comments). But I disagree this should be pushed down to the representations - I'd like those to be ignorant of astronomy! Though once we move the option to special-case to get_mask(), I can see pushing an option to just do angles down to SpecialRepresentation.get_mask().
|
OK, now implemented a default mask that combines representation and possible differentials. It makes things quite a bit simpler, so that argues that it was the right change to make. Check PR changelog error is because this is based on the mask for representations PR, #16845, which is not yet merged. |
| [(15.00000668, 3.00000101, 0.1), ( ———, ———, ———)]> | ||
|
|
||
| In the last example, you will notice that the angles of the second item have | ||
| become masked too. This is because the distance is used in the conversion |
There was a problem hiding this comment.
Not for this PR, but for frame transforms that are only rotations we could avoid going to the cartesian representation, instead doing change of basis stuff on the rotation matrix.
There was a problem hiding this comment.
Yes, indeed, that would be much nicer. I guess that would need for the Spherical case to first transform to UnitSpherical, and then add the distance back in. Actually, I think you already have set this up for Spherical.transform, so it would only be a matter of using it inside transformations.
...
A quick tests shows that indeed spherical.transform(m) properly already propagates the mask - angular coordinates keep the angular mask (I'll push the test to #16845). But transformations bypasses this by first converting to cartesian; that should be solvable, but will need a bit more thought.
There was a problem hiding this comment.
I wonder if it might be as simple as taking the frame transformation and adding a change of basis on either side to match the input and output representation types. For Cartesian this would end up being the identity matrix.
There was a problem hiding this comment.
The problematic code is
astropy/astropy/coordinates/transformations/affine.py
Lines 118 to 131 in 1b82175
I tried a simple removal of the conversion to cartesian, since the matrix multiplication can be handled by the different representation classes internally. Unfortunately, adding the offset right now does require moving to cartesian. Obviously, this is solvable too, but best in a different PR! (EDIT: the simplest solutions would involve going back from Cartesian to Spherical in cases where one does not want that; possibly, the .transform method could be upgraded to also have an optional offset?)
There was a problem hiding this comment.
I agree that we shouldn't try to change how transformations work in this pull request, but perhaps it would be better to use a different example in our documentation. For example, when converting from ICRS to GCRS distance determines parallax, so we do expect the on-the-sky coordinates to become masked when the distance is masked.
There was a problem hiding this comment.
Yes, sounds like a good idea to change the transformation example -- and maybe mention in the general warning about this being new that sometimes masks may propagate further than expected.
There was a problem hiding this comment.
We should open an issue so that any users who might want to report this wouldn't have to write one up themselves, but doing that can wait until the feature freeze has happened.
eerovaher
left a comment
There was a problem hiding this comment.
Based on the tests and the documentation the public API seems mostly reasonable. The one question I have is whether the output of get_mask() should be a structured array so that its components could be accessed by the name of the corresponding attribute. This would be especially useful when get_mask() is called without any arguments.
The documentation should be expanded. For example, get_mask() doesn't have any documentation outside its docstring, but it really should.
| return out | ||
|
|
||
|
|
||
| class CoordinateSharedMethods: |
There was a problem hiding this comment.
What's the reasoning of having masked and mask here? Is it just to keep the masking-related code together?
|
|
||
| class CoordinateSharedMethods: | ||
| @property | ||
| def masked(self): |
There was a problem hiding this comment.
Why don't you annotate at least the functions that are simple to annotate?
| def masked(self): | |
| def masked(self) -> bool: |
There was a problem hiding this comment.
I still don't typically think of typing at all, but, yes, I can do that. If we keep the mixin, that actually would be a nice bit of self-contained annotated code (is it ndarray[bool] for .mask?)
EDIT: looking at numpy, I guess it would be np.ndarray[Any, np.dtype[np.bool]]or np.ndarray[tuple[int, ...], np.dtype[np.bool]]. Yikes.
There was a problem hiding this comment.
With numpy.typing.NDArray the annotation can be simplified to NDArray[np.bool].
There was a problem hiding this comment.
I'm going to leave this to a typing PR; time is definitely running out.
There was a problem hiding this comment.
I am not allowed to demand that you write the annotations.
There was a problem hiding this comment.
sorry, this really just a matter of spending time wisely - once a module is typed, I think requiring the rest to remain typed is fine, of course.
| mask = property( | ||
| get_mask, | ||
| doc="""The mask associated with these coordinates. | ||
|
|
||
| Combines the masks of all components of the underlying representation, | ||
| including possible differentials. | ||
| """, | ||
| ) |
There was a problem hiding this comment.
Why is property not being used as a decorator?
There was a problem hiding this comment.
It would be an extra function call (as it would just contain return self.get_mask()).
There was a problem hiding this comment.
Mypyc should be able to optimize away such intermediate function calls, but we are still very far from compiling coordinates.
If you think this is code is that performance critical then the implementation makes sense, but do add a comment.
There was a problem hiding this comment.
It is just standard use of property - it is certainly not performance critical.
There was a problem hiding this comment.
Agreed. This happens in the Python docs https://docs.python.org/3/library/functions.html#property and is reasonable when we want a public getter.
| Like more generally for |Masked| instances, one can access the mask of using | ||
| ``.mask``, check whether the underlying data are masked using ``masked``, | ||
| access underlying unmasked data using ``.unmasked``, and get a new instance | ||
| with masked data replaced with a fill value using the ``filled()`` method. |
There was a problem hiding this comment.
Why aren't there any tests for this?
There was a problem hiding this comment.
Because it just uses __setitem__... But fair enough, one should at least have one test!
| ``.get_mask()`` method. For instance, ``sc.get_mask("ra", "dec")`` would | ||
| get the combined masks of just the angular components, which may be useful | ||
| if dealing with, e.g., a catalogue of stars where distances are available | ||
| only for some of the objects. |
There was a problem hiding this comment.
It should also be mentioned that the frame attributes can be masked too and accessed with get_mask().
There was a problem hiding this comment.
Arguably too much for the what's-new, but would definitely be good to add to the actual docs.
| *attrs : str | ||
| Attributes from which to get the masks to combine. Items can be | ||
| dotted, like ``"data.lon", "data.lat"``. By default, get the | ||
| combined mask of all components (including from differentials). |
There was a problem hiding this comment.
The docstring should mention that frame attributes can accessed too, but they are not accessed by default.
| Attributes from which to get the masks to combine. Items can be | ||
| dotted, like ``"data.lon", "data.lat"``. By default, get the | ||
| combined mask of all components (including from differentials). | ||
|
|
There was a problem hiding this comment.
Why is there no Returns section?
The way this is written, I think not - it is meant to give a array of plain bool back. It would not be too hard to extend (e.g., with a |
|
In particular there is very little support for structured arrays outside of numpy. Supporting it as an option is fine, less so as the default. |
|
Let me take this out of the thread, since it is less about For
I really do not like (3). Maybe the cleanest (as far as possible) is to go with (1) for now - I am traveling next 2 weeks but can probably do a quick bugfix for both |
|
OK, did the push, and tests pass. Very happy to see that table joining now works properly! And, just to reiterate it, I'm very much in favour of following this up with adding other methods like |
|
Thanks, @nstarman, @eerovaher, @taldcroft and @StuartLittlefair - it's going in! |
Original PR #17016 by mhvk Original: astropy/astropy#17016
Merged from original PR #17016 Original: astropy/astropy#17016
Original PR #17016 by mhvk Original: astropy/astropy#17016
Merged from original PR #17016 Original: astropy/astropy#17016
This pull request is the follow-up to #16845, using the mask on representations in
SkyCoordand coordinate frames. It also includes brief docs and a what's-new entry.Notes:
(hence opened as draft for now), but feel free to just look at the whole here;.maskproperty and other features in the what's-new. I tried just adding:inherited-members:toref_api.py, but that leads to large numbers of failures that seem out of scope of this PR.Maskedapproach, that masks just get propagated. For some methods, likeapply_space_motion, as well as for coordinate transformations, it might make more sense to treat masked values truly as missing ones, so that, e.g., a masked distance is taken to be the same as if the coordinate hadUnitSphericalrepresentation. The question then is whether that is in fact what the user wants, i.e., to what extent one should refuse the temptation to guess...