Skip to content

fix: Coordinate comparisons - #131

Merged
icweaver merged 4 commits into
mainfrom
iw/separation
Nov 10, 2026
Merged

icweaver merged 4 commits into
mainfrom
iw/separation

Conversation

@icweaver

@icweaver icweaver commented Nov 8, 2026

Copy link
Copy Markdown
Member

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

@icweaver icweaver linked an issue Nov 8, 2026 that may be closed by this pull request
@codecov

codecov Bot commented Nov 8, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
⚠️ Please upload report for BASE (main@2772f44). Learn more about missing BASE report.

Additional details and impacted files
@@           Coverage Diff           @@
##             main     #131   +/-   ##
=======================================
  Coverage        ?   96.47%           
=======================================
  Files           ?       10           
  Lines           ?      454           
  Branches        ?        0           
=======================================
  Hits            ?      438           
  Misses          ?       16           
  Partials        ?        0           

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@cgarling cgarling left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think this looks good overall. Keeping == strict and pointing users to separation(a, b) < tol seems correct. The inline suggestions fix a precision loss when a projected coordinate's offsets are more precise than its origin and add regression tests.

I noticed that _unprojected_type can also fix an existing bug that this PR didn't introduce. cartesian fails on a nested projection like ProjectedCoords(cp, zero(cp.offset)), because it only looks one origin deep. I couldn't leave a suggestion because the line is outside the diff, but this change to src/projected.jl:81 fixes it (it needs the inline _unprojected_type suggestion applied first):

cartesian(c::AbstractProjectedCoords) = CartesianCoords{_unprojected_type(typeof(c))}(coords2cart(c))

Comment thread src/projected.jl Resolved
Comment thread test/runtests.jl Resolved
Comment thread test/runtests.jl Resolved
@icweaver
icweaver merged commit 8061e90 into main Nov 10, 2026
10 of 11 checks passed
@icweaver
icweaver deleted the iw/separation branch November 10, 2026 17:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

research: Coordinate equality

2 participants