| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
Sorry, something went wrong.
|
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.
|
Sorry, something went wrong.
… element-wise attribute handling
Detailed explanation of _frameattr_equiv vs. _frameattr_eq_elementwiseClick to expand The two helpers sit next to each other in [baseframe.py:1551](astropy/coordinates/baseframe.py#L1551) and [baseframe.py:1604](astropy/coordinates/baseframe.py#L1604), and they look near-duplicated. This section walks the two side by side and explains, for each block, whether it is genuinely the same code or only superficially similar.Short version: the dispatch skeleton is identical and the four leaf comparisons Block 1 — signature: identical@staticmethod
def _frameattr_equiv(left_fattr, right_fattr): # noqa: PLR0911
def _frameattr_eq_elementwise(left_fattr, right_fattr): # noqa: PLR0911Both are staticmethods taking two raw attribute values, and both trip ruff's Block 2 — identity and None shortcuts: identicalif left_fattr is right_fattr:
return True
elif left_fattr is None or right_fattr is None:
return FalseByte-for-byte the same (the new one has an extra clause in the comment noting Worth knowing how load-bearing that first line is: because Attribute.__get__ Note this shortcut is also where both helpers return a plain scalar True Block 3 — representation type-mismatch guard: identicalleft_is_repr = isinstance(left_fattr, r.BaseRepresentationOrDifferential)
if left_is_repr ^ isinstance(right_fattr, r.BaseRepresentationOrDifferential):
return FalseIdentical. A representation compared against a non-representation is False Block 4 — representation body: substantially differentOld: if getattr(left_fattr, "differentials", False) or getattr(right_fattr, "differentials", False):
warnings.warn("... at least one of them has differentials. This yields False "
"even if the underlying representations are equivalent ...", AstropyWarning)
return False
return np.all(
left_fattr == right_fattr
if type(left_fattr) is type(right_fattr)
else left_fattr.to_cartesian() == right_fattr.to_cartesian()
)New: left_diffs = getattr(left_fattr, "differentials", {})
right_diffs = getattr(right_fattr, "differentials", {})
if left_diffs.keys() != right_diffs.keys() or any(
type(left_diffs[key]) is not type(right_diffs[key]) for key in left_diffs
):
return False
if type(left_fattr) is not type(right_fattr):
if left_diffs or any(isinstance(fattr, r.BaseDifferential)
for fattr in (left_fattr, right_fattr)):
return False
left_fattr = left_fattr.to_cartesian()
right_fattr = right_fattr.to_cartesian()
return left_fattr == right_fattrThis is the block that actually diverges, and it is the only semantic change
Block 5 — coordinate type-mismatch guard: identicalleft_is_coord = isinstance(left_fattr, BaseCoordinateFrame)
if left_is_coord ^ isinstance(right_fattr, BaseCoordinateFrame):
return FalseIdentical, same reasoning as block 3. Block 6 — coordinate body: different, but convergingOld: return left_fattr.is_equivalent_frame(right_fattr) and np.all(left_fattr == right_fattr)New: if left_fattr.__class__ is not right_fattr.__class__:
return False
if left_fattr.has_data != right_fattr.has_data:
return False
return left_fattr == right_fattrBoth exist for the same reason, stated in the old docstring: comparing The new version guards the same hazard, but the hazard shrank. After this PR, Using is_equivalent_frame here would have been wrong for the new helper: it Block 7 — fallback: differs only by np.all()return np.all(left_fattr == right_fattr) # old
return left_fattr == right_fattr # newThis is the path taken by Time, Quantity and EarthLocation attributes — 10.1 Could they be merged?Nearly. Running both over 25 representative attribute pairs and comparing
The first is the intentional improvement (sub-decision 5). The second is a 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
Recommendation: land this PR as-is with the duplication, then follow up with a |
Sorry, something went wrong.
|
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 SkyCoord and Frame. I certainly could (and am willing) to split this into two PRs:
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. |
Sorry, something went wrong.
|
Can you give us a feeling for how urgent this is? I haven't had time to provide a detailed review of the pull request yet, but from a first glance much of the complexity seems to arise from how BaseCoordinateFrame handles equality and much of that code is liable to change given the work towards APE 23. Since that APE is supposed to be stripping data out of the frame classes, it arguably makes much of this work redundant. If its not urgent, perhaps we can delay until the APE23 work is done. Although perhaps there is still merit in hashing out what equality for SkyCoordinate object should mean now... |
Sorry, something went wrong.
|
@StuartLittlefair - thanks for the response. There is no particular urgency on this PR. It basically stems from a long-standing (somewhat low priority) issue that grouping a Table by a SkyCoord object doesn't work because SkyCoord equality is a bit broken. So I stepped out of my lane to take a crack at fixing it. I'm happy enough to put a hold on this PR or leave it as a demonstration of what could be done. I might see what it would look like to make an isolated change purely in SkyCoord that defines equality as all the attributes (frame attributes and "extra" attributes) and data being equal in a broadcasted sense. If there is any discussion related to defining equality I'd be happy see it. |
Sorry, something went wrong.
| Back | FazBrowse Home | New Git URL |
Overview
This PR was originally motivated by work on Table.group_by(), where trying to group by a SkyCoord revealed a known issue that SkyCoord equality itself was inconsistent and gave surprising results. Opening this can of worms led to API changes in the BaseCoordinateFrame class as well. I believe these changes are for the better, but
input from coordinates maintainers is obviously critical here.
Comparing SkyCoord or BaseCoordinateFrame objects with == now takes frame attributes such as equinox, obstime or location into 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 obstime that matches for only some of the coordinates now gives a partially True result::
AI disclosure
This PR is almost entirely AI-generated using Claude Opus 5. I have examined the code
and fully understand the changes in SkyCoord and the tests. But I must be honest
and say that the changes in BaseCoordinateFrame look reasonable to me but I could not
assess 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 expandCompare coordinate frame attributes element-wise instead of requiring equivalence
== on SkyCoord and BaseCoordinateFrame currently refuses to answer whenever the
two operands differ in any frame attribute. This is inconsistent with how the
coordinate data is treated — a single differing element there gives a partially
True array, not an exception — and it is inconsistent with itself, since the same
attribute (obstime) behaves differently depending on whether it happens to be a
primary 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_frame is deliberately untouched.
All examples below were run on main and on this branch; the outputs are verbatim.
Common setup:
12.1 Primary frame attribute mismatch: TypeError → element-wise
main:
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-wise
main:
branch:
Together with §12.1 this closes the inconsistency in §5.1: obstime now behaves the
same way whether or not it belongs to the coordinate's own frame.
12.3 Extra attribute present on only one side: ValueError → all-False
main: the same ValueError as §12.2 →
branch: array([False, False])
An attribute set on one side and unset on the other is a genuine difference, so it
compares False rather than raising. This matches what _frameattr_equiv already
did for the None case.
12.4 Representation attributes with differentials: warn-and-False → compared properly
Setup (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 SkyCoord can carry an array-valued extra attribute. The comparison result now
reflects 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
is_equivalent_frame is untouched, so everything that gates on it — item assignment,
concatenate, frame transformations — behaves exactly as before. Only == / !=
change.
12.7 Implementation
mirroring _frameattr_equiv's branch structure but returning the element-wise result
instead of collapsing it with np.all. _frameattr_equiv and is_equivalent_frame
are untouched.
mismatch. It compares self._data == value._data first, so shape mismatches still
produce the informative message from the representation classes, then &s in
_frameattr_eq_elementwise over self.frame_attributes.
frame comparison result rather than raising, using np.logical_and so the result
broadcasts against array-valued extra attributes (§12.5).
See §10 for a block-by-block comparison of _frameattr_equiv and
_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 frame
attributes will now get an array instead. Code that catches TypeError/ValueError
around == to detect "not comparable" should use is_equivalent_frame instead, which
is unchanged. A docs/changes/coordinates/20211.api.rst fragment and a
docs/whatsnew/8.1.rst entry are included.