Fix the D_H node sign convention and guard the acos domain in Dcriteria - #96
Conversation
calcDSH, calcDD and calcDH clip the argument of asin but not the argument of
acos. For the inclination term this argument is
cos(i1)*cos(i2) + sin(i1)*sin(i2)*cos(O2 - O1)
which for two identical orbits reduces to cos^2 i + sin^2 i. That sum rounds to
slightly above 1.0 in double precision for a large fraction of inclinations, and
math.acos then raises
ValueError: math domain error
Measured over an 18001-point inclination sweep from 0 to 180 deg, comparing one
orbit with itself:
D_SH 768/18001 = 4.27% of inclinations raise
D_H 768/18001 = 4.27%
D_D 1197/18001 = 6.65% (two acos calls: I_21 and theta_21)
D_D fails more often because calcDD also passes the angular separation of the two
perihelion directions through an unguarded acos.
This is not an edge case that only a synthetic test reaches. It fires on any
self-comparison, on Monte Carlo clones of one orbit, and on every diagonal
element of a distance matrix, which is exactly what a shower-association or
clustering run computes first.
Fixed by applying the clipping idiom already used for asin in this file to every
acos argument, including theta_21 in calcDD.
The guard changes no value that previously computed successfully: it only engages
when the argument exceeds 1 by about one ulp, where acos would otherwise have
raised. A self-comparison still cannot return exactly 0.0, because acos has an
infinite derivative at 1 and so turns a 1 ulp argument error into about 2e-8 in
the returned angle. Measured worst case over the same sweep:
D_SH max |D| = 2.107e-08 exactly 0.0 in 14662/18001 cases
D_H max |D| = 2.107e-08 exactly 0.0 in 14662/18001
D_D max |D| = 8.188e-09 exactly 0.0 in 12422/18001
The tests therefore assert a self-comparison is zero to within 1e-7 rather than
exactly zero. Both new tests fail on the parent commit with "math domain error".
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
calcDH computed the difference of the longitudes of perihelion as
pi21 = w2 - w1 + 2*asin(...)
without the rho = +/-1 factor that calcDSH applies. Southworth & Hawkins (1963)
specify the negative branch when |O2 - O1| > 180 deg, and Jopek (1993) adopts
that definition of Pi unchanged when replacing the perihelion distance term. The
factor is not optional: without it D_H depends on how the ascending node happens
to be written.
Worked example, q = 0.5 AU, e = 0.7, i = 15 deg, O1 = 10 deg, w1 = 150 deg,
w2 = 190 deg, with the second node written the two equivalent ways:
before: O2 = 350 deg -> 0.69872 O2 = -10 deg -> 0.26675
after: O2 = 350 deg -> 0.26675 O2 = -10 deg -> 0.26675
The same orbit pair returned two different dissimilarities depending only on
whether the node was written as 350 deg or as -10 deg.
Size of the error. The rho factor enters only the last D_H term, which is bounded
by ((e1 + e2)/2)^2 * 4, so the discrepancy is bounded by 2*e. Maximising
|D_H(before) - D_H(after)| over the physical parameter box subject to
|O2 - O1| > 180 deg attains 1.9800, exactly 2*e_max for e_max = 0.99. A plain
Monte Carlo sample of 50214 pairs with |O2 - O1| > 180 deg reaches 1.1478.
Consequence for shower association. Drawing close pairs whose nodes straddle
0/360 deg (nodes within 20 deg either side of 0, partner orbit perturbed by 5% in
q, 0.03 in e, 3 deg in i and 5 deg in w), 97379 of 2000000 pairs come out
associated at D_H < 0.10 under at least one node representation. Of those, 36.6%
are placed on opposite sides of the threshold by the two representations.
The fix copies the rho block from calcDSH verbatim and multiplies the asin term
by it, so the two functions now differ only in the perihelion distance term, as
intended.
Tests. Three of the four new tests check properties rather than tabulated values:
- test_DH_invariant_under_node_representation: rewriting the second node across
the 0/360 deg cut must not change D_H. Note the rho test is not 2*pi periodic,
so the convention assumes both nodes are given in [0, 360 deg); the test
reduces the node difference to its principal value rather than adding a full
turn.
- test_DH_coplanar_limit_recovers_longitude_of_perihelion: for i1 = i2 = 0,
Pi_21 must equal (w2 + O2) - (w1 + O1). This pins the sign of rho outright.
Only the corrected branch satisfies it; before the fix it is off by 1.3991.
- test_DH_equals_DSH_when_perihelia_sum_to_unity: D_H and D_SH differ only in
normalising the perihelion term by (q1 + q2), so they must agree exactly when
q1 + q2 = 1. Before the fix they disagree by up to 0.83942.
- test_DSH_invariant_under_node_representation is a control: it passes both
before and after, confirming calcDSH was already correct and that the test
itself is not simply asserting the new behaviour.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The sign of rho was previously established from properties alone: node
representation invariance, the coplanar limit, and consistency with calcDSH. This
adds an independent check against a second published definition of Pi_21, from
the author of D_H itself.
Jopek & Bronikowska (2017), P&SS 143, 43, eqs 4, 5 and 7, define Pi_21 through the
mutual node of the two orbital planes,
N = h_1 x h_2, Pi_21 = angle(N, e_1) - angle(N, e_2)
with h the unit angular momentum and e the unit Laplace vector. There is no
arcsine and so no branch to choose, which makes it decisive about the sign.
Measured over 200000 random orbit pairs, comparing sin(Pi_21/2) squared, the
quantity that actually enters D_SH and D_H:
with rho agrees on 200000/200000 = 100.00%, max difference 1.5e-11
without rho agrees on 149453/200000 = 74.73%, max difference 1.0
Restricted to the |O2 - O1| > 180 deg branch, which is where rho acts:
with rho agrees on 50547/50547 = 100.00%
without rho agrees on 0/50547 = 0.00%
Not one of the 50547 straddling pairs agrees without the factor. rho is exactly
what reconciles the arcsine form with the branch-cut-free vector form.
The new test works through the public return value rather than an internal
quantity: with q1 = q2 and e1 = e2 the first two terms of D_H vanish, so
sin(Pi_21/2) squared can be recovered from what calcDH returns. It fails on the
parent commit by 0.99933, against a maximum possible discrepancy of 1.
One caveat on the reference. Eq. 7 as printed writes each angle as arccos(N.e),
which is unsigned and cannot tell which side of the node a perihelion lies on;
taken literally it agrees with the corrected arcsine form only 49.95% of the time.
The helper resolves each angle within its own orbital plane with atan2 about that
orbit's normal. This is the same class of omission as the arccos written for an
atan2 elsewhere in this literature, and is noted in the helper's docstring so the
next reader does not transcribe eq. 7 as printed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
A third independent primary source for the sign convention, found while checking a Jenniskens (2008), Icarus 194, 13, eq. 3 states the rule explicitly: "the sign of While there, I checked a discrepancy in that same equation, since it would affect
No change is warranted — the codebase is right and eq. 3 as printed is a For completeness, section 2.1 of that paper carries two further transcription No code in this PR changed as a result of any of this. 🤖 Generated with Claude Code |
The fix itself is unchanged; this removes the duplication it left behind and writes down the convention it implements. - _clippedAcos() replaces the clipping idiom that had been written out at four sites, and carries the explanation of why the guard is needed at all. - _mutualNodeAngles() returns the mutual inclination and the difference of the longitudes of perihelion, which calcDSH and calcDH now share verbatim. The two criteria differ only in normalising the perihelion distance term, which is what the PR restored, so the shared part belongs in one place. - The two pre-existing "Name sure" typos went with the blocks they were in. Verified value-identical over 100000 random orbit pairs: calcDSH and calcDD are unchanged to the last bit, both against this PR as submitted and against master. calcDH moves by at most 1.9e-13, because master wrote its arcsine argument as `*1/math.cos(...)` where calcDSH wrote `*(1/math.cos(...))`, and the two associate differently in floating point. Folding them onto one expression costs one ulp and is what makes them genuinely the same code. Both docstrings now state the node branch rule and, more usefully, its limit: the rho test is on the raw node difference, so it assumes both nodes lie in [0, 360 deg). A node carrying an extra full turn selects the wrong branch. That is inherent to the Southworth & Hawkins convention and applies to calcDSH just as it does to calcDH. Three tests added, 7 to 10: - The worked example from the PR description, pinned at 0.26675 for both representations of the node, so the convention cannot be quietly reverted. - The 2*pi limitation, asserted as the documented behaviour rather than papered over, so that changing it later is a visible decision. - _clippedAcos, checking that it passes through everything math.acos could already compute and only engages one ulp outside the domain. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
D_SH and D_H choose the branch of an arcsine from the raw difference of the two ascending nodes, so the convention holds only when both are given in [0, 360). A node written as 710 rather than 350 selects the other branch and returns a different value. This is the limitation documented by the companion fix in wmpg#96, and it belongs here because this is the page a user reads before choosing a criterion. The criteria built on vectors instead of an arcsine, the rho family and calcDVJopek, have no branch to choose. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Review pass: 1 commit pushed to this branchBoth bugs are real, the fix is right, and the test suite is the best in the four PRs I have reviewed this round. Independent confirmationMeasured myself rather than taken from the description:
Note your headline 36.6 % measures something different — two node representations of the same pair, not
The literature agrees. During the #97 review an agent verified over 200,000 pairs that your fixed
|
| Agreement with Jopek's vector Pi_21, nodes carrying random extra turns | |
|---|---|
Your rho rule |
78.0 % |
rho from the principal value of dOmega |
50.3 % — worse |
| Nodes in [0, 360 deg), your rule | 100 % |
So the rule cannot be repaired by reducing the angle. You were right to leave it, and I have pinned the behaviour in a test rather than pretending otherwise.
But the robust construction is already in this PR — _piFromVectorDefinition at test_Dcriteria.py:186. It builds Pi_21 from the mutual node N = h1 x h2, has no arcsine and therefore no branch to choose, and is invariant under any node representation. For nodes in [0, 360 deg) it agrees with the fixed code to 3e-11, i.e. numerically invisible.
Worth considering promoting it from the test file into calcDSH and calcDH, which would remove the limitation outright. I did not do it here, for two reasons: it changes calcDSH, which this PR deliberately leaves alone and uses as its control, and D_SH is the most-used criterion in the field — a change there deserves its own decision rather than riding along in a D_H fix. Your call, and the maintainer's.
Smaller notes
- Test naming:
test_snake_casehere,testCamelCaseeverywhere else in wmpl. Already raised on Add further orbit dissimilarity criteria, orbit classification and threshold methods #97 covering both PRs; I left yours alone and matched local style in what I added. - The standalone runner (
python -m wmpl.Utils.Tests.test_Dcriteria) still works and now reports 10.
Merges cleanly with master and with #97; 54 tests pass here, 208 on the #97 branch.
🤖 Generated with Claude Code
Brings in the D_H node sign fix and the acos domain guards merged as wmpg#96. The branch was five commits behind, so its own command line reported the pre-fix D_H: PR wmpg#96's worked example (q = 0.5, e = 0.7, i = 15 deg, O1 = 10 deg, w1 = 150 deg, w2 = 190 deg, O2 = 350 deg) came out as 0.698722 where master gives 0.266746. That is now consistent. Merges without conflict; 223 tests pass on the result.
Two correctness fixes in
wmpl/Utils/Dcriteria.py, kept separate from any newfunctionality so they can be reviewed on their own. One commit per bug, plus the
tests.
calcDSH,calcDD,calcDN,calcDVandcalcDVuncertkeep theircurrent behaviour except for the acos guard described below.
1.
calcDHwas missing the node sign conventioncalcDHcomputed the difference of the longitudes of perihelion aswithout the
rho = ±1factor thatcalcDSHalready applies. Southworth &Hawkins (1963) specify the negative branch when
|O2 - O1| > 180 deg, and Jopek(1993) adopts that definition of
Piunchanged when replacing the periheliondistance term. Without it,
D_Hdepends on how the ascending node happens to bewritten.
Worked example,
q = 0.5 AU,e = 0.7,i = 15 deg,O1 = 10 deg,w1 = 150 deg,w2 = 190 deg, with the second node written the two equivalentways:
O2 = 350 degO2 = -10 degThe same pair of orbits returned two different dissimilarities depending only on
the representation of the node.
Size of the error.
rhoenters only the last term, which is bounded by((e1 + e2)/2)^2 * 4, so the discrepancy is bounded by2*e. Maximising|D_H(before) - D_H(after)|over the physical parameter box subject to|O2 - O1| > 180 degattains 1.9800, exactly2*e_maxfore_max = 0.99. Aplain Monte Carlo sample of 50214 pairs on that branch reaches 1.1478.
Consequence for shower association. Drawing close pairs whose nodes straddle
0/360 deg (nodes within 20 deg either side of 0, partner perturbed by 5% in
q,0.03 in
e, 3 deg ini, 5 deg inw), 97379 of 2000000 pairs come outassociated at
D_H < 0.10under at least one node representation. Of those,36.6% are placed on opposite sides of the threshold by the two representations.
The fix copies the
rhoblock fromcalcDSHverbatim and multiplies theasinterm by it, so the two functions now differ only in the perihelion distance term.
2. Unguarded
acosdomaincalcDSH,calcDDandcalcDHclip the argument ofasinbut not the argumentof
acos. For the inclination term that argument reduces tocos^2 i + sin^2 ifor two identical orbits, which rounds above 1.0 in double precision, and
math.acosthen raisesValueError: math domain error.Measured over an 18001-point inclination sweep from 0 to 180 deg, comparing one
orbit with itself:
D_SHD_HD_DD_Dfails more often becausecalcDDalso passes the angular separation of thetwo perihelion directions (
theta21) through an unguardedacos.This is not reachable only by a synthetic test. It fires on any self-comparison,
on Monte Carlo clones of one orbit, and on every diagonal element of a distance
matrix, which is the first thing a shower-association or clustering run computes.
Fixed by applying the clipping idiom already used for
asinin this file to everyacosargument, includingtheta21. The guard changes no value that previouslycomputed successfully: it engages only when the argument exceeds 1 by about one
ulp, where
acoswould otherwise have raised.A self-comparison still cannot return exactly 0.0, because
acoshas an infinitederivative at 1 and turns a 1 ulp argument error into about 2e-08 in the returned
angle. Measured worst case over the same sweep: 2.107e-08 for
D_SHandD_H,8.188e-09 for
D_D. The tests therefore assert zero to within 1e-07.Tests
New file
wmpl/Utils/Tests/test_Dcriteria.py, 7 tests. Runs under pytest orstandalone via
python -m wmpl.Utils.Tests.test_Dcriteria, matching theconvention in
test_AlphaBeta.py.Six of the seven fail on
master; the seventh is a deliberate control.test_DH_pi21_matches_the_vector_definition— the strongest check. Jopek &Bronikowska (2017), P&SS 143, 43, eqs 4, 5 and 7 define
Pi_21through themutual node of the two orbital planes,
N = h_1 x h_2, with no arcsine and sono branch to choose. Comparing
sin(Pi_21/2)^2, the quantity that actuallyenters
D_H, over 200000 random pairs: withrho200000/200000 = 100.00%agreement, max difference 1.5e-11; without
rho149453/200000 = 74.73%.Restricted to the
|O2 - O1| > 180 degbranch: withrho50547/50547 =100.00%, without
rho0/50547 = 0.00%. Not one straddling pair agreeswithout the factor. The test works through the public return value, not an
internal quantity. It fails on
masterby 0.99933 against a maximum possiblediscrepancy of 1.
test_DH_coplanar_limit_recovers_longitude_of_perihelion— fori1 = i2 = 0,Pi_21must equal(w2 + O2) - (w1 + O1). Off by 1.3991 onmaster.test_DH_equals_DSH_when_perihelia_sum_to_unity— the two differ only innormalising the perihelion term by
(q1 + q2), so they must agree exactly whenq1 + q2 = 1. Disagree by up to 0.83942 onmaster.test_DH_invariant_under_node_representation— rewriting the second nodeacross the 0/360 deg cut must not change
D_H.test_DSH_invariant_under_node_representation— the control. Passes beforeand after, confirming
calcDSHwas already correct and that the suite is notmerely asserting the new behaviour.
test_acos_domain_holds_on_self_comparisonandtest_acos_domain_holds_for_near_identical_orbits— both fail onmasterwithmath domain error.One caveat on the invariance test, worth knowing before reviewing it: the
rhotest
|O2 - O1| > piis not 2pi periodic, so the convention holds only when bothnodes are given in
[0, 360 deg). Adding a full turn to a node can push|dOmega|past 3pi, where the rule breaks forcalcDSHtoo. That is apre-existing property of the Southworth & Hawkins convention and is not addressed
here; the test reduces the node difference to its principal value instead, which
is the case that matters in practice.
Notes
stated above so they can be reproduced.
__main__blockis untouched.
master, not from this one,so the two are independent and this fix is not blocked by a scope discussion.
🤖 Generated with Claude Code