Make computing angular separations less surprising#16246
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.
|
5d22b5d to
df7e169
Compare
|
I'll write the What's New entry after the code and documentation changes are reviewed. The RTD build is failing because Sphinx can't figure out how to link to the new type alias I've introduced. This can be addressed by adding a new entry to |
mhvk
left a comment
There was a problem hiding this comment.
Yes, makes sense, since in most cases it may really not be what the user expects (the common case will be that the coordinates are in the same frame, after all).
Comments about the details.
df7e169 to
4015e1f
Compare
mhvk
left a comment
There was a problem hiding this comment.
Looks good, only a single suggestion left - but feel free to ignore it!
| def __init__( | ||
| self, frame_to: BaseCoordinateFrame, frame_from: BaseCoordinateFrame | ||
| ) -> None: | ||
| self.frame_to = frame_to |
There was a problem hiding this comment.
I'd tend to swap these two, but no big deal to keep it as is (I can see the nicety of self, other_frame in the actual initialization...)
There was a problem hiding this comment.
I prefer to keep things convenient for the caller, so I'm leaving this as is.
|
p.s. Personally not sure this needs a what's-new, since in the end this is a relatively small change, but leave that decision up to you. |
neutrinoceros
left a comment
There was a problem hiding this comment.
This definitely feels like the right approach, and thank you for testing it extensively. I have 2 suggestions which I can summarize as:
- the new public facing argument should be keyword-only
- we could use an enum internally to represent valid values for it. Originally I thought of this as a way to enforce exhaustiveness at type-check time, but as I progressed through my review I more or less convinced myself it wasn't worth the effort since it could make it much more awkward to display a nice error message for invalid values, as you did. Anyway, feel free to disregard these comments !
astropy/coordinates/baseframe.py
Outdated
| from astropy.coordinates import Latitude, Longitude, SkyCoord | ||
| from astropy.units import Unit | ||
|
|
||
| StrictnessControl: TypeAlias = Literal["ignore", "warn", "error"] |
There was a problem hiding this comment.
while we're at it using the type system to annotate this, we might as well utilize an actual enum and leverage typing.assert_never
There was a problem hiding this comment.
additional notes on this:
typing.assert_neveris new in Python 3.11 but available intyping_extensions- maybe surprisingly (but in a good way), it does have an effect at runtime
- using a
StrEnumwould also allow users to pass in plain strings, which would preserve the API you're adding enum.StrEnumis also new in Python 3.11, but is very light weight so it's easy to backport for 3.10
astropy/coordinates/baseframe.py
Outdated
| if not ( | ||
| frame_mismatch == "ignore" |
There was a problem hiding this comment.
If using an enum as I'm suggesting, this part could be made easier to swallow for type checkers as:
if frame_mismatch is StrictnessControl.IGNORE:
pass
elif ...:
...| elif frame_mismatch == "error": | ||
| raise NonRotationTransformationError(self, other_frame) | ||
| else: | ||
| raise ValueError( |
There was a problem hiding this comment.
In passing, assert_never(frame_mistmatch) would also allow runtime validation and elegantly replace this. That said, I recognize that the custom error message is much clearer as is, especially since it's intended to end users. Keeping it doesn't prevent using an enum though.
astropy/coordinates/baseframe.py
Outdated
| def separation( | ||
| self, | ||
| other: BaseCoordinateFrame | SkyCoord, | ||
| frame_mismatch: StrictnessControl = "warn", |
There was a problem hiding this comment.
I feel like since Python 2 was dropped there hasn't been a compelling reason to not make every new arguments to public functions keyword-only (which matches how you're illustrating its use in docs anyway)
| frame_mismatch: StrictnessControl = "warn", | |
| *, | |
| frame_mismatch: StrictnessControl = "warn", |
There was a problem hiding this comment.
Making positional-or-keyword arguments keyword-only is a backwards incompatible change, but making keyword-only arguments positional-or-keyword is backwards compatible. So I agree that it is better to err on the side of caution and make this keyword-only in the public API.
astropy/coordinates/baseframe.py
Outdated
| The coordinate to get the separation to. | ||
| frame_mismatch : {"warn", "ignore", "error"} | ||
| If the ``other`` coordinates are in a different frame then they | ||
| will have to be transformed and depending on the details |
There was a problem hiding this comment.
Could you clarify what details ? ("details of the transformation" would be enough, provided that's what you mean here)
| @@ -0,0 +1,9 @@ | |||
| The ``SkyCoord`` and ``BaseCoordinateFrame`` ``separation()`` methods now | |||
| accept an optional ``frame_mismatch`` argument. | |||
There was a problem hiding this comment.
Together with my previous suggestion to make the argument keyword-only from the get go.
| accept an optional ``frame_mismatch`` argument. | |
| accept an optional ``frame_mismatch`` keyword argument. |
| @@ -0,0 +1,9 @@ | |||
| The ``SkyCoord`` and ``BaseCoordinateFrame`` ``separation()`` methods now | |||
There was a problem hiding this comment.
It would maybe feel more natural to rephrase this changelog entry as "A warning is now emitted by default in case ... . It is also possible to suppress this warning or to promote it to an error via the new keyword argument ...", instead of introducing the new argument first.
|
|
||
| @pytest.mark.parametrize("coord_class", [SkyCoord, ICRS]) | ||
| @pytest.mark.parametrize( | ||
| "frame_mismatch_kwarg,expectation", |
There was a problem hiding this comment.
extremely minor but while we're at it
| "frame_mismatch_kwarg,expectation", | |
| "frame_mismatch_kwarg, expectation", |
There was a problem hiding this comment.
Ruff has a few rules for enforcing a consistent style in fixtures and parametrizations, including PT006. I brought these rules up in the developer telecon last September, but the overall opinion was that enforcing them is not worth the effort, so currently contributors are free to use whatever style they like, as long as pytest can parse it.
I use comma separated values (not comma-and-space separated values) because that is what the examples in pytest documentation use.
4015e1f to
9ea56b8
Compare
|
FWIW, I agree with the suggestion of a keyword-only argument. I like the principle of a |
|
There's a few other places in astropy/astropy/utils/iers/iers.py Lines 190 to 191 in 014036d I agree that using Enum or StrEnum would be good idea, but it should be done consistently throughout astropy so it's beyond the scope for this pull request.
This pull request closes 4 issues, and there are several closed duplicate issues. Clearly this is something that is important to the users and deserves to be highlighted. I'll have to push another commit with the What's New entry, but everything else can be reviewed already. |
Dully noted, thank you both. This pattern is growing more and more onto my lately so I'm sure I'll remember to try it out at scale if an opportunity arises. Anyway, back to this PR: thanks @eerovaher for addressing my comments, I'm happy to sign this off now ! |
When computing the angular separation between coordinates in different frames they must first be transformed into a common frame. However, the resulting angular separation value can depend on the direction of the transformation, unless it is a pure rotation. Users have found this to be surprising, so by default a warning is now emitted if a non-rotation transformation is needed. It is possible to always suppress the warning, or to forbid non-rotation transformations. Computing the position angle always performs the transformation silently because its value is expected to depend on the order of the coordinates anyways.
9ea56b8 to
e7b9590
Compare
|
I have made two changes: the |
mhvk
left a comment
There was a problem hiding this comment.
This looks all good. The biggest problem is of course naming: you are right that frame_mismatch was not that good -- since some frame mismatches are fine while others are not -- but origin_mismatch to me at first glance suggests that it matters relative to what origin the separation is measured from -- while from the (very nice!) what's new it becomes clear you mean the origin of the coordinate system.
Other options might be "frame_dependent", "ambiguous", "non_associative"...
I'll approve now but won't merge yet in case a better name comes up...
neutrinoceros
left a comment
There was a problem hiding this comment.
I second Marten: this what's new entry is indeed excellent. Just a very minor comment but otherwise I think it's perfect.
Regarding the name of the keyword argument, I didn't think of this before you renamed it but agree that the new name is more accurate. Marten has a point that it may be slightly confusing, though I don't have a better suggestion to make at the moment.
docs/whatsnew/6.1.rst
Outdated
| Telescope are) the Sun and the Moon will be (almost) in the same direction. | ||
| The :meth:`~astropy.coordinates.BaseCoordinateFrame.separation` method | ||
| automatically converts a coordinate given to it to the frame of the coordinate | ||
| it belongs to, but the separation can be different if the coordinates are |
There was a problem hiding this comment.
| it belongs to, but the separation can be different if the coordinates are | |
| it belongs to, so the separation can be different if the coordinates are |
e7b9590 to
61a7686
Compare
|
I merged the pull request that made |
Description
When computing the angular separation between coordinates in different frames they first need to be transformed into a common frame. However, the resulting angular separation value can depend on the direction of the transformation, unless it is a simple rotation. Users have found this to be surprising, so by default a warning should be emitted if a non-rotation transformation is needed. It should also be possible to always suppress the warning, or to forbid non-rotation transformations.
Computing the position angle can always be done silently because its value is expected to depend on the order of the coordinates anyways.
Closes #8505, closes #12189, closes #11388, closes #14812