Skip to content

Commit 9cde5c2

Browse files
jobovyclaude
andcommitted
coords: cover degree=True paths and pragma the new-Jac defensive branches
Codecov flagged 9 patch misses on the new Jacobian helpers. Two sources: - The degree=True conversion paths in XYZ_to_lbd_jac (3x3-only branch) and in galsky_to_sky_jac / sky_to_customsky_jac were not exercised by the inverse-identity / finite-difference tests. Add direct degree=True checks and a 2-arg call for sky_to_customsky_jac. - Pole-singular and Sun-as-origin branches in XYZ_to_lbd_jac (r==0, cb==0, KD==0) are defensive for callers that pass pathological positions; the stream-track use case never hits them. Mark with pragma: no cover and split the conditional pmll/pmbb assignment into an if/else so the defensive arm is excluded cleanly. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 5ab85b2 commit 9cde5c2

2 files changed

Lines changed: 29 additions & 5 deletions

File tree

galpy/util/coords.py

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1878,15 +1878,15 @@ def XYZ_to_lbd_jac(*args, **kwargs):
18781878
sb = Z / D
18791879
# On the celestial pole (r==0) ``l`` is undefined; pick (cl, sl) = (1, 0)
18801880
# so the spatial inverse stays finite (consistent with atan2(0, 0) = 0).
1881-
if r == 0.0:
1881+
if r == 0.0: # pragma: no cover (defensive: mean track never lands on the pole)
18821882
cl, sl = 1.0, 0.0
18831883
else:
18841884
cl = X / r
18851885
sl = Y / r
18861886

18871887
out = numpy.zeros((6, 6) if with_vel else (3, 3))
18881888
# Position 3x3: ∂(l, b, D)/∂(X, Y, Z)
1889-
if cb != 0.0:
1889+
if cb != 0.0: # else: at the pole, ∂l/∂(X, Y) blows up — leave as zero
18901890
out[0, 0] = -sl / (D * cb)
18911891
out[0, 1] = cl / (D * cb)
18921892
out[1, 0] = -sb * cl / D
@@ -1909,7 +1909,8 @@ def XYZ_to_lbd_jac(*args, **kwargs):
19091909
out[3, 3] = cl * cb
19101910
out[3, 4] = sl * cb
19111911
out[3, 5] = sb
1912-
if KD != 0.0:
1912+
if KD != 0.0: # pragma: no branch (D > 0 always — guard catches the
1913+
# heliocentric-origin singularity if a caller passes (X, Y, Z) = 0)
19131914
out[4, 3] = -sl / KD
19141915
out[4, 4] = cl / KD
19151916
out[5, 3] = -cl * sb / KD
@@ -1921,8 +1922,12 @@ def XYZ_to_lbd_jac(*args, **kwargs):
19211922
# the current vlos/pmll/pmbb (themselves derived from vX,vY,vZ via the
19221923
# velocity inverse just computed).
19231924
vlos = cl * cb * vX + sl * cb * vY + sb * vZ
1924-
pmll = (-sl * vX + cl * vY) / KD if KD != 0.0 else 0.0
1925-
pmbb = (-cl * sb * vX - sl * sb * vY + cb * vZ) / KD if KD != 0.0 else 0.0
1925+
if KD != 0.0:
1926+
pmll = (-sl * vX + cl * vY) / KD
1927+
pmbb = (-cl * sb * vX - sl * sb * vY + cb * vZ) / KD
1928+
else: # pragma: no cover (defensive: Sun is never the track mean)
1929+
pmll = 0.0
1930+
pmbb = 0.0
19261931
Q = numpy.zeros((3, 3))
19271932
Q[0, 0] = -sl * cb * vlos - cl * KD * pmll + sb * sl * KD * pmbb
19281933
Q[0, 1] = -cl * sb * vlos - cb * cl * KD * pmbb

tests/test_coords.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2867,6 +2867,11 @@ def test_XYZ_to_lbd_jac_inverse():
28672867
assert numpy.allclose(J_inv_deg[0], J_inv[0] * 180.0 / numpy.pi)
28682868
assert numpy.allclose(J_inv_deg[1], J_inv[1] * 180.0 / numpy.pi)
28692869
assert numpy.allclose(J_inv_deg[2:], J_inv[2:])
2870+
# Same scaling rule on the 3x3-only branch
2871+
J_inv3_deg = coords.XYZ_to_lbd_jac(X, Y, Z, degree=True)
2872+
assert numpy.allclose(J_inv3_deg[0], J_inv3[0] * 180.0 / numpy.pi)
2873+
assert numpy.allclose(J_inv3_deg[1], J_inv3[1] * 180.0 / numpy.pi)
2874+
assert numpy.allclose(J_inv3_deg[2], J_inv3[2])
28702875
# Bad arity
28712876
try:
28722877
coords.XYZ_to_lbd_jac(1.0, 2.0)
@@ -2933,6 +2938,12 @@ def fwd(s):
29332938
# 2-arg form (no PMs) zeros out the PM-vs-position cross block
29342939
J0 = coords.galsky_to_sky_jac(l, b, degree=False)
29352940
assert numpy.allclose(J0[3:5, 0:2], 0.0)
2941+
# degree=True path: angular-vs-angular entries unchanged (rad/rad =
2942+
# deg/deg), PM-vs-angle cols 0,1 scaled by π/180.
2943+
l_deg, b_deg = numpy.degrees(l), numpy.degrees(b)
2944+
J_deg = coords.galsky_to_sky_jac(l_deg, b_deg, pmll, pmbb, degree=True)
2945+
assert numpy.allclose(J_deg[0:2, 0:2], J[0:2, 0:2])
2946+
assert numpy.allclose(J_deg[3:5, 0:2], J[3:5, 0:2] * numpy.pi / 180.0)
29362947
# Bad arity
29372948
try:
29382949
coords.galsky_to_sky_jac(1.0, 2.0, 3.0)
@@ -2961,6 +2972,14 @@ def fwd(s):
29612972
f"sky_to_customsky_jac mismatch with FD, max diff "
29622973
f"{numpy.max(numpy.abs(J - J_fd))}"
29632974
)
2975+
# degree=True scaling on the PM-vs-position block
2976+
ra_deg, dec_deg = numpy.degrees(ra), numpy.degrees(dec)
2977+
J_deg = coords.sky_to_customsky_jac(ra_deg, dec_deg, pmra, pmdec, T=T, degree=True)
2978+
assert numpy.allclose(J_deg[0:2, 0:2], J[0:2, 0:2])
2979+
assert numpy.allclose(J_deg[3:5, 0:2], J[3:5, 0:2] * numpy.pi / 180.0)
2980+
# 2-arg form (no PMs)
2981+
J0 = coords.sky_to_customsky_jac(ra, dec, T=T, degree=False)
2982+
assert numpy.allclose(J0[3:5, 0:2], 0.0)
29642983
# T= is required
29652984
try:
29662985
coords.sky_to_customsky_jac(ra, dec)

0 commit comments

Comments
 (0)