Improve SkyCoord and Frame equality operators - #20211
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.
|
… element-wise attribute handling
Detailed explanation of
|
| case | _frameattr_equiv |
np.all(elementwise) |
|---|---|---|
| representation attrs with equal differentials | False + AstropyWarning |
True |
| coordinate attr, one has data and the other does not | raises ValueError |
False |
The first is the intentional improvement (sub-decision 5). The second is a
pre-existing bug in _frameattr_equiv, and therefore in
is_equivalent_frame, which is documented as returning bool and raising only
TypeError. It is reachable from the public API, because
CoordinateAttribute.convert_input passes a value through untouched when it is
already an instance of the right frame class, data or not:
g1 = Galactocentric(..., galcen_coord=ICRS(1*u.deg, 2*u.deg))
g2 = Galactocentric(..., galcen_coord=ICRS()) # no data, accepted
g1.is_equivalent_frame(g2)
# ValueError: cannot compare: one frame has data and the other does notSo _frameattr_equiv could in principle become:
@staticmethod
def _frameattr_equiv(left_fattr, right_fattr):
return bool(np.all(BaseCoordinateFrame._frameattr_eq_elementwise(left_fattr, right_fattr)))deleting ~35 lines and the duplicated dispatch entirely. That was not done
here, deliberately:
- It would change
is_equivalent_frame, and through it__setitem__,
concatenate,spherical_offsets_toand transform loopback detection — frames
whose representation attributes carry equal differentials would become
"equivalent" where they previously were not. That is almost certainly the
correct behavior, but it is a second API change with a much wider blast radius
than the one this PR is making, and it deserves its own review. - The
ValueErrorfix is a genuine bugfix that stands on its own and would be
easier to review as a separate PR againstmain.
Recommendation: land this PR as-is with the duplication, then follow up with a
PR that fixes the is_equivalent_frame ValueError and collapses
_frameattr_equiv into a np.all() wrapper. Splitting it that way keeps the
is_equivalent_frame semantic change reviewable on its own terms rather than
riding along inside an equality change.
|
In astropy/astropy-project#538 (comment), @astrofrog commented that this is an example where PRs should be split into smaller parts where possible. I strongly agree with the principle. In this particular case I'm not so sure because one of the key outcomes of this PR is unifying the behavior for equality of
The hitch here is first that (1) is not hugely valuable on its own. Another hitch is that both of these are API changes that would merit a What's New, so (1) would provide an intermediate version and then (2) the final version. Because these changes are intrinsically coupled I'm not convinced that splitting into two PR's will reduce the overall level of effort. |
Overview
This PR was originally motivated by work on
Table.group_by(), where trying to group by aSkyCoordrevealed a known issue thatSkyCoordequality itself was inconsistent and gave surprising results. Opening this can of worms led to API changes in theBaseCoordinateFrameclass as well. I believe these changes are for the better, butinput from coordinates maintainers is obviously critical here.
Comparing
SkyCoordorBaseCoordinateFrameobjects with==now takes frame attributes such asequinox,obstimeorlocationinto account element-wise, in the same way as the coordinate data itself. Previously any difference in a frame attribute raised an exception, even when only a single element of an array-valued attribute differed.For example, an
obstimethat matches for only some of the coordinates now gives a partiallyTrueresult::AI disclosure
This PR is almost entirely AI-generated using Claude Opus 5. I have examined the code
and fully understand the changes in
SkyCoordand the tests. But I must be honestand say that the changes in
BaseCoordinateFramelook reasonable to me but I could notassess myself the full impact (the blast radius as Claude says). This code base is
sufficiently complex that making this assessment is beyond my expertise. To that end
I have directed Claude to provide a clear statement of what is changed and I hope that
(along with testing) this is sufficient for the maintainer experts.
Detailed Description
Click to expand
Compare coordinate frame attributes element-wise instead of requiring equivalence
==onSkyCoordandBaseCoordinateFramecurrently refuses to answer whenever thetwo operands differ in any frame attribute. This is inconsistent with how the
coordinate data is treated — a single differing element there gives a partially
Truearray, not an exception — and it is inconsistent with itself, since the sameattribute (
obstime) behaves differently depending on whether it happens to be aprimary attribute of the frame or a
SkyCoord"extra" attribute (§5.1).This PR makes frame attributes and extra frame attributes contribute to the
comparison element-wise, exactly like the data. Comparing coordinates of different
frame classes still raises, since there is no element-wise answer in that case, and
is_equivalent_frameis deliberately untouched.All examples below were run on
mainand on this branch; the outputs are verbatim.Common setup:
12.1 Primary frame attribute mismatch:
TypeError→ element-wisemain:branch:The same holds one tier down, on bare frames:
main:TypeError: cannot compare: objects must have equivalent frames: ...→branch:array([ True, False])12.2 Extra frame attribute mismatch:
ValueError→ element-wisemain:branch:Together with §12.1 this closes the inconsistency in §5.1:
obstimenow behaves thesame way whether or not it belongs to the coordinate's own frame.
12.3 Extra attribute present on only one side:
ValueError→ all-Falsemain: the sameValueErroras §12.2 →branch:array([False, False])An attribute set on one side and unset on the other is a genuine difference, so it
compares
Falserather than raising. This matches what_frameattr_equivalreadydid for the
Nonecase.12.4 Representation attributes with differentials: warn-and-
False→ compared properlySetup (a frame with a representation-valued attribute that retains differentials):
main— note this fires even when the attributes are equal:branch— no warning, and the two cases are now distinguished:This retires the warning's own "this may change in future versions of Astropy" promise.
12.5 Result shape now accounts for extra-attribute shape
Extra frame attributes are not broadcast against the data when they are set, so a
scalar
SkyCoordcan carry an array-valued extra attribute. The comparison result nowreflects that shape instead of collapsing it:
main:np.True_→branch:array([ True, True])This is what makes a partially-matching extra attribute on a scalar coordinate
expressible at all, and it resolves §5.2.
12.6 What deliberately does not change
mainICRSvsFK5)TypeErrorTypeError(unchanged)is_equivalent_framewith differingobstimeFalseFalse(unchanged)concatenateof non-equivalent framesValueErrorValueError(unchanged)TrueTrue(unchanged)FalseFalse(unchanged)ValueError: cannot compare: shape mismatch: ...array([ True, False])is_equivalent_frameis untouched, so everything that gates on it — item assignment,concatenate, frame transformations — behaves exactly as before. Only==/!=change.
12.7 Implementation
baseframe.py: newBaseCoordinateFrame._frameattr_eq_elementwisestatic method,mirroring
_frameattr_equiv's branch structure but returning the element-wise resultinstead of collapsing it with
np.all._frameattr_equivandis_equivalent_frameare untouched.
baseframe.py:BaseCoordinateFrame.__eq__raisesTypeErroronly on a classmismatch. It compares
self._data == value._datafirst, so shape mismatches stillproduce the informative message from the representation classes, then
&s in_frameattr_eq_elementwiseoverself.frame_attributes.sky_coordinate.py:_extra_frameattr_equivfolds the extra attributes into theframe comparison result rather than raising, using
np.logical_andso the resultbroadcasts against array-valued extra attributes (§12.5).
See §10 for a block-by-block comparison of
_frameattr_equivand_frameattr_eq_elementwise, and §10.1 for why they are not merged in this PR.12.8 Backwards compatibility
This is an API change: code that relied on
==raising for mismatched frameattributes will now get an array instead. Code that catches
TypeError/ValueErroraround
==to detect "not comparable" should useis_equivalent_frameinstead, whichis unchanged. A
docs/changes/coordinates/20211.api.rstfragment and adocs/whatsnew/8.1.rstentry are included.