Skip to content

Make computing angular separations less surprising#16246

Merged
mhvk merged 2 commits intoastropy:mainfrom
eerovaher:separation-frame-mismatch
Mar 29, 2024
Merged

Make computing angular separations less surprising#16246
mhvk merged 2 commits intoastropy:mainfrom
eerovaher:separation-frame-mismatch

Conversation

@eerovaher
Copy link
Member

@eerovaher eerovaher commented Mar 27, 2024

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

  • By checking this box, the PR author has requested that maintainers do NOT use the "Squash and Merge" button. Maintainers should respect this when possible; however, the final decision is at the discretion of the maintainer that merges the PR.

@github-actions
Copy link
Contributor

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.

  • Do the proposed changes actually accomplish desired goals?
  • Do the proposed changes follow the Astropy coding guidelines?
  • Are tests added/updated as required? If so, do they follow the Astropy testing guidelines?
  • Are docs added/updated as required? If so, do they follow the Astropy documentation guidelines?
  • Is rebase and/or squash necessary? If so, please provide the author with appropriate instructions. Also see instructions for rebase and squash.
  • Did the CI pass? If no, are the failures related? If you need to run daily and weekly cron jobs as part of the PR, please apply the "Extra CI" label. Codestyle issues can be fixed by the bot.
  • Is a change log needed? If yes, did the change log check pass? If no, add the "no-changelog-entry-needed" label. If this is a manual backport, use the "skip-changelog-checks" label unless special changelog handling is necessary.
  • Is this a big PR that makes a "What's new?" entry worthwhile and if so, is (1) a "what's new" entry included in this PR and (2) the "whatsnew-needed" label applied?
  • At the time of adding the milestone, if the milestone set requires a backport to release branch(es), apply the appropriate "backport-X.Y.x" label(s) before merge.

@eerovaher eerovaher force-pushed the separation-frame-mismatch branch from 5d22b5d to df7e169 Compare March 27, 2024 15:42
@eerovaher
Copy link
Member Author

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 docs/nitpick-exceptions.

Copy link
Contributor

@mhvk mhvk left a comment

Choose a reason for hiding this comment

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

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.

@eerovaher eerovaher force-pushed the separation-frame-mismatch branch from df7e169 to 4015e1f Compare March 27, 2024 17:56
Copy link
Contributor

@mhvk mhvk left a comment

Choose a reason for hiding this comment

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

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
Copy link
Contributor

Choose a reason for hiding this comment

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

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...)

Copy link
Member Author

Choose a reason for hiding this comment

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

I prefer to keep things convenient for the caller, so I'm leaving this as is.

@mhvk
Copy link
Contributor

mhvk commented Mar 27, 2024

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.

Copy link
Contributor

@neutrinoceros neutrinoceros left a comment

Choose a reason for hiding this comment

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

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 !

from astropy.coordinates import Latitude, Longitude, SkyCoord
from astropy.units import Unit

StrictnessControl: TypeAlias = Literal["ignore", "warn", "error"]
Copy link
Contributor

Choose a reason for hiding this comment

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

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

Copy link
Contributor

Choose a reason for hiding this comment

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

additional notes on this:

  • typing.assert_never is new in Python 3.11 but available in typing_extensions
  • maybe surprisingly (but in a good way), it does have an effect at runtime
  • using aStrEnum would also allow users to pass in plain strings, which would preserve the API you're adding
  • enum.StrEnum is also new in Python 3.11, but is very light weight so it's easy to backport for 3.10

Comment on lines +1764 to +1765
if not (
frame_mismatch == "ignore"
Copy link
Contributor

Choose a reason for hiding this comment

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

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(
Copy link
Contributor

Choose a reason for hiding this comment

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

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.

def separation(
self,
other: BaseCoordinateFrame | SkyCoord,
frame_mismatch: StrictnessControl = "warn",
Copy link
Contributor

Choose a reason for hiding this comment

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

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)

Suggested change
frame_mismatch: StrictnessControl = "warn",
*,
frame_mismatch: StrictnessControl = "warn",

Copy link
Member Author

Choose a reason for hiding this comment

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

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.

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
Copy link
Contributor

Choose a reason for hiding this comment

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

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.
Copy link
Contributor

Choose a reason for hiding this comment

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

Together with my previous suggestion to make the argument keyword-only from the get go.

Suggested change
accept an optional ``frame_mismatch`` argument.
accept an optional ``frame_mismatch`` keyword argument.

@@ -0,0 +1,9 @@
The ``SkyCoord`` and ``BaseCoordinateFrame`` ``separation()`` methods now
Copy link
Contributor

Choose a reason for hiding this comment

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

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",
Copy link
Contributor

Choose a reason for hiding this comment

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

extremely minor but while we're at it

Suggested change
"frame_mismatch_kwarg,expectation",
"frame_mismatch_kwarg, expectation",

Copy link
Member Author

Choose a reason for hiding this comment

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

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.

Copy link
Contributor

Choose a reason for hiding this comment

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

fair enough !

@eerovaher eerovaher force-pushed the separation-frame-mismatch branch from 4015e1f to 9ea56b8 Compare March 28, 2024 14:26
@mhvk
Copy link
Contributor

mhvk commented Mar 28, 2024

FWIW, I agree with the suggestion of a keyword-only argument. I like the principle of a StrEnum, but feel that if we go that route, we should do that in a separate PR for more functions (and perhaps only when our minimum python version is 3.11, since really there is no hurry).

@eerovaher
Copy link
Member Author

There's a few other places in astropy that allow users to either suppress warnings or to raise errors instead. For example, here's a configuration item in utils that uses the exact same values as frame_mismatch:

iers_degraded_accuracy = _config.ConfigItem(
["error", "warn", "ignore"],

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.

p.s. Personally not sure this needs a what's-new, since in the end this is a relatively small change

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.

@neutrinoceros
Copy link
Contributor

neutrinoceros commented Mar 28, 2024

I like the principle of a StrEnum, but feel that if we go that route, we should do that in a separate PR for more functions (and perhaps only when our minimum python version is 3.11, since really there is no hurry).

There's a few other places in astropy that allow users to either suppress warnings or to raise errors instead.

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.
@eerovaher eerovaher force-pushed the separation-frame-mismatch branch from 9ea56b8 to e7b9590 Compare March 28, 2024 20:36
@eerovaher
Copy link
Member Author

I have made two changes: the frame_mismatch argument has been renamed to the more appropriate origin_mismatch and the documentation now uses "pure rotation" instead of "simple rotation". I have also added a second commit with the What's New entry.

@eerovaher eerovaher requested a review from mhvk March 28, 2024 20:42
Copy link
Contributor

@mhvk mhvk left a comment

Choose a reason for hiding this comment

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

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...

Copy link
Contributor

@neutrinoceros neutrinoceros left a comment

Choose a reason for hiding this comment

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

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.

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
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
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

@eerovaher eerovaher force-pushed the separation-frame-mismatch branch from e7b9590 to 61a7686 Compare March 29, 2024 16:29
@eerovaher
Copy link
Member Author

I merged the pull request that made Cosmology a dataclass and that caused a merge conflict in the What's New file. I solved the conflict by changing the order of the sections in that file and I made some small changes in the process.

Copy link
Contributor

@mhvk mhvk left a comment

Choose a reason for hiding this comment

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

Thanks!

@mhvk mhvk merged commit a7c5891 into astropy:main Mar 29, 2024
@eerovaher eerovaher deleted the separation-frame-mismatch branch March 29, 2024 17:11
eerovaher added a commit to eerovaher/astropy that referenced this pull request Apr 23, 2024
eerovaher added a commit to eerovaher/astropy that referenced this pull request Apr 23, 2024
@pllim pllim mentioned this pull request Apr 23, 2024
1 task
pllim added a commit that referenced this pull request Apr 23, 2024
meeseeksmachine pushed a commit to meeseeksmachine/astropy that referenced this pull request Apr 23, 2024
pllim added a commit that referenced this pull request Apr 23, 2024
…333-on-v6.1.x

Backport PR #16333 on branch v6.1.x (Fix #16246 change log entry)
d-giles referenced this pull request in d-giles/astropy-abdufork Jul 26, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

3 participants