Repository navigation
Implement __eq__ and __ne__ for coordinate classes - #10154
Conversation
|
I do think we should have comparisons, but I wonder about just returning |
| if self._data is None and value._data is None: | ||
| return True | ||
|
|
||
| if self.shape != value.shape: |
There was a problem hiding this comment.
Independently of my larger comment, I would allow different shapes (but catch the possible broadcast error and return False), i.e., sc[0] == sc should be possible.
| if isinstance(eq, bool): | ||
| return not eq | ||
| else: | ||
| return ~eq |
There was a problem hiding this comment.
Wow, never appreciated that ~True equals -2!
There was a problem hiding this comment.
what? how? why? (I could see it it were -1, but -2?)
There was a problem hiding this comment.
As @jdavies-st explained it to me, ~ flips the bits and somehow True is treated as int when ~ is applied.
>>> np.binary_repr(1, width=8)
'00000001'
>>> np.binary_repr(-2, width=8)
'11111110'There was a problem hiding this comment.
Isn't __ne__ automagically defined if __eq__ is defined in python3? Does __eq__ as defined above ever return something other than True or False?
Btw, I'm really happy to see __eq__ for strict equality implemented for SkyCoord. Will make testing much cleaner. 🎉
There was a problem hiding this comment.
I'm not sure the automagic __ne__ would be numpy-aware, which is necessary since it can return a numpy array of bool type. Also, SkyCoord already has a __ne__ method that comes from somewhere but I honestly don't know where.
In [7]: sc.__ne__??
Signature: sc.__ne__(value, /)
Call signature: sc.__ne__(*args, **kwargs)
Type: method-wrapper
String form: <method-wrapper '__ne__' of SkyCoord object at 0x7f7f29204850>
Docstring: Return self!=value.
There was a problem hiding this comment.
I see. That makes sense.
In [13]: ~np.array([True, True, False])
Out[13]: array([False, False, True])
Thanks. I'd forgot that ~ acting on numpy boolean arrays is the same as np.logical_not. 👍
There was a problem hiding this comment.
Thanks for the reminder on np.logical_not. That works on both scalar and np.array values so I got rid of that ugly code.
Agreed. I'll make that change. I wasn't entirely happy with
I'm not sure how that helps in this case since we are stipulating that no transforms are allowed. So at best you can get a false positive where the representation values are extremely close but not equal, and after doing the separation math going down to a single float you wind up with exact equality. Also separations are just spatial and don't have visibility into velocities AFAIK.
Good idea. Along with the first point (raise, not False), this might point to factoring out all or most of the validation checks, maybe to private |
|
BTW, about |
|
Indeed, and people might want different definitions of closeness in distance than in angular coordinates. It is also a bit similar to |
| with pytest.raises(ValueError, match='unable to compare column c'): | ||
| t1.values_equal([1, 2]) | ||
|
|
||
| with pytest.raises(TypeError, match='comparison for column sk'): |
There was a problem hiding this comment.
This used to fail, no more!
|
Fingers crossed on CI tests (passes locally) but this should be mostly done and ready for review if anyone has the inclination. BTW I'm not planning to work on |
|
So now I have some higher-level questions. It turns out that this change caused real test failures in WCS. At some level I think that the failures are good because they revealed code which could be improved. But also point to ways that this PR as it stands might cause breakage in the real world. The comparisons in WCS are: astropy/astropy/visualization/wcsaxes/core.py Line 586 in d91f444 Boiling down the issue:
Other considerations:
|
b15724d to
0412d6c
Compare
|
Part of the issue is that python and numpy have different ideas about what Right now, in |
|
p.s. If we're not raising on unequal frames, I think in any case where we returning |
|
@mhvk - the current version stopped short of the slippery slope and only will allow the comparison of unequal frames if they both have no data. I do feel like In the current version one does still get a TypeError for And for BTW, I have been seeing the various numpy deprecations for what seems like years. When is that going to finally happen? |
|
@astrofrog - this PR required making some small changes in visualizations/wcsaxes. Git blame says this code is yours so it would be nice if you can review them. |
|
Humm, Travis seems to be unhappy, twice in a row I got the "oldest dependencies" job running over an hour and finally quitting. The corresponding tox test runs locally just fine. |
|
@taldcroft , the old deps job should be fixed once #10164 is merged. Just need approval... |
|
The CI failure workaround has been merged, so a rebase should fix travis here. |
0412d6c to
a89e382
Compare
eteq
left a comment
There was a problem hiding this comment.
Generally I'm happy with this. While I think I originally was opposed to equality like this, time has shown the value of this strict definition requiring exact equality of the data. Some caveats/concerns I have (all of which I think are not major deals/blockers, except possibly the last one which requires agreement).
I think we need to show the user how to do a "tolerance" comparison (I made an inline suggestion you can just accept as-is if you agree, @taldcroft) as early as possible. That was the original reason for disallowing this, to force them to encounter that, so I'm thinking we want to show it as prominently as practical.
Re: the discussion of False's vs the more numpy-like result of failing when comparisons are invalid: I think I like the behavior as it stands right now - I do see it as reasonably consistent with Time (which I think is what @mhvk was saying?) while still having some of the more practical comparisons like the FK4()==ICRS() case work is indeed a compromise but one I think that makes sense for the actual likely things people will do with this operator. So 👍 to that behavior as-is.
However, I noticed the following behavior, which seems rather counter-intuitive:
>>> sc1=coordinates.SkyCoord(ra=0*u.deg, dec=0*u.deg, distance=1*u.kpc)
>>> sc2=coordinates.SkyCoord(sc1.data.to_cartesian(), frame='icrs')
>>> sc1==sc2
True
>>> i1=coordinates.ICRS(ra=0*u.deg, dec=0*u.deg, distance=1*u.kpc)
>>> i2=coordinates.ICRS(i1.data.to_cartesian())
>>> i1==i2
TypeError: cannot compare: objects must have same class: SphericalRepresentation vs. CartesianRepresentation
This comes down to the fact that something in the SkyCoord initializer I think is coercing the representation, which is fine but it hides the confusing behavior in the representations. This behavior becomes even more obvious as a confusion in the following:
>>> i1.data+i2.data-i2.data == i1.data
True
>>> i2.data+i1.data-i2.data == i1.data
TypeError: cannot compare: objects must have same class: CartesianRepresentation vs. SphericalRepresentation
Basically, for arithmetic we make the assumption that representations can swap types (basically they all convert to cartesian and then back if need be), but we don't allow that behavior in equality.
So I'd vote for modifying the representation equality behavior to either do direct comparison if the representations are the same (i.e., the behavior in this PR), but if they are not the same, to convert both to cartesian and also try to do the equality test there. Then the above cases I think should work (modulo possible floating-point... but at least they won't raise errors)
|
OK all thanks for the review and feedback, this got to a much better place. As @adrn has approved I will merge if this can get through CI before another changelog merge conflict. |
Co-authored-by: Adrian Price-Whelan <[email protected]>
|
I think this broke a test... Please see #10297 |
This PR keeps `==` strict and documents `separation(a, b) < tol` as the
on-sky comparison. Making that the recommended route exposed a bug with
projected coordinates, so `separation`, `position_angle`, and `offset`
are fixed here as well.
## Usage
Comparing two descriptions of the same point on the sky:
```julia-repl
julia> c = ICRSCoords(1.0, 0.2);
julia> c == convert(GalCoords, c)
false
julia> separation(c, convert(GalCoords, c)) < 1e-9
true
```
Projected coordinates now work as the left argument:
```julia-repl
julia> c0 = ICRSCoords(0.1, -0.2); c1 = ICRSCoords(0.10001, -0.19997);
julia> p = SkyCoords.project(c0, c1);
julia> separation(p, c0) # Was: ArgumentError
3.15603173906638e-5
julia> position_angle(p, c0) # Was: ArgumentError
3.4573512132621755
julia> offset(p, c0) # Was: ArgumentError
(3.15603173906638e-5, 3.4573512132621755)
julia> offset(p, 1e-5, 0.5) # Was: MethodError
ProjectedCoords{ICRSCoords{Float64}, Float64}(ICRSCoords{Float64}(0.1, -0.2), [1.459488348425262e-5, 3.8775827948173136e-5])
```
## Design
### Comparison semantics
`==` keeps its current meaning: same frame, same representation, and
exactly equal values. Converting first would not make it reliable,
because conversions round. Over 100,000 random ICRS points, a round trip
through another frame returned identical values for only 9% to 26% of
them, depending on the frame, with a worst error around 1e-15.
Converting into the left argument's frame would also make `==`
asymmetric: `convert(GalCoords, c) == c` would be `true` while `c ==
convert(GalCoords, c)` would usually be `false`. Converting into a fixed
frame like ICRS restores symmetry, but returns `false` for about 85% of
ICRS/Galactic pairs.
This matches astropy, which deliberately does no conversion in `==`
(astropy/astropy#10154), returns `False` for `SkyCoord`s in different
frames starting with 8.1 (astropy/astropy#20266), and points users to
`separation` for tolerance checks.
`≈` is unchanged. It is not defined between spherical coordinates in
different frames today, so extending it later would not break anything
if there is demand.
### Projected coordinates
The mixed-type methods converted the right argument to the left
argument's type, which cannot work for a projected left argument because
its origin is a value rather than a type parameter. `separation` and
`position_angle` now compare in the origin's frame instead, following
nested projections through `_unprojected_type`. The two-coordinate
`offset` is built from those two, so it needs no change of its own.
Every other coordinate type maps to itself, and same-type `convert`
returns its input, so existing results do not change.
`offset(p, sep, pa)` documents that it returns the input's type, so the
result is re-projected around the same origin, sharing its arithmetic
with `project` through `_project_offset`. This is handled inside the
existing `offset` method because a dedicated `::AbstractProjectedCoords`
method would be ambiguous with the DynamicQuantities extension's
`offset` methods.
---------
Co-authored-by: Chris Garling <[email protected]>
Description
This pull request implements a strict equality operator for coordinate classes
SkyCoord,BaseFrame,BaseRepresentationOrDifferential, andBaseRepresentation.Strict equality means that if any of the attributes don't match then an exception is raised.
The logic is inspired by #9857, having realized that this PR went most of the way to defining when these coordinate objects can be compared meaningfully using the representation data.
This will help in testing and bring consistency with the general numpy array interface. In 4.0 one gets silly things like this:
Fixes #3182