fix: 修复天象事件 API 在事件边界附近的重复返回问题
- 修正合月、合日、留等Last/Next/Closest接口在精确命中和事件后秒级查询时仍返回当前事件的问题,避免应用侧枚举卡死 - 收紧事件边界判定,并补强水星、金星、火星近事件路径的稳定化,保证公开API的单调前进语义 - 补充公开wrapper、跨世纪样本和外部基线回归,覆盖合月邻近事件与边界场景
This commit is contained in:
parent
be3af3884c
commit
46b555cd49
@ -2,14 +2,17 @@ package basic
|
|||||||
|
|
||||||
import "math"
|
import "math"
|
||||||
|
|
||||||
const exactEventTolerance = 2.0 / 86400.0
|
const (
|
||||||
|
exactEventTolerance = 2.0 / 86400.0
|
||||||
|
exactQueryTTToleranceUT = 0.1 / 86400.0
|
||||||
|
)
|
||||||
|
|
||||||
func sameEventJD(a, b float64) bool {
|
func sameEventJD(a, b float64) bool {
|
||||||
return math.Abs(a-b) <= exactEventTolerance
|
return math.Abs(a-b) <= exactEventTolerance
|
||||||
}
|
}
|
||||||
|
|
||||||
func sameEventUTQueryTT(eventUT, queryTT float64) bool {
|
func sameEventUTQueryTT(eventUT, queryTT float64) bool {
|
||||||
return math.Abs(eventUTQueryTTDelta(eventUT, queryTT)) <= exactEventTolerance
|
return math.Abs(eventUTQueryTTDelta(eventUT, queryTT)) <= exactQueryTTToleranceUT
|
||||||
}
|
}
|
||||||
|
|
||||||
func closestEventUTToQueryTT(queryTT, best float64, candidates ...float64) float64 {
|
func closestEventUTToQueryTT(queryTT, best float64, candidates ...float64) float64 {
|
||||||
|
|||||||
@ -2,7 +2,11 @@ package basic
|
|||||||
|
|
||||||
import "math"
|
import "math"
|
||||||
|
|
||||||
const innerEventEpsilon = 4.0 / 86400.0
|
const (
|
||||||
|
innerEventEpsilon = 0.1 / 86400.0
|
||||||
|
innerEventWindowPadding = 4.0 / 86400.0
|
||||||
|
innerEventMaximizeEpsilon = 4.0 / 86400.0
|
||||||
|
)
|
||||||
|
|
||||||
func eventQueryTTAsUT(queryTT float64) float64 {
|
func eventQueryTTAsUT(queryTT float64) float64 {
|
||||||
return TD2UT(queryTT, false)
|
return TD2UT(queryTT, false)
|
||||||
@ -157,7 +161,7 @@ func maximizeInWindow(start, end, coarseStep float64, coarseFn, exactFn func(flo
|
|||||||
guess := scanWindowForMax(start, end, coarseStep, coarseFn)
|
guess := scanWindowForMax(start, end, coarseStep, coarseFn)
|
||||||
left := clampFloat64(guess-coarseStep, start, end)
|
left := clampFloat64(guess-coarseStep, start, end)
|
||||||
right := clampFloat64(guess+coarseStep, start, end)
|
right := clampFloat64(guess+coarseStep, start, end)
|
||||||
if right-left <= innerEventEpsilon {
|
if right-left <= innerEventMaximizeEpsilon {
|
||||||
return guess
|
return guess
|
||||||
}
|
}
|
||||||
for i := 0; i < 20; i++ {
|
for i := 0; i < 20; i++ {
|
||||||
|
|||||||
@ -1,6 +1,9 @@
|
|||||||
package basic
|
package basic
|
||||||
|
|
||||||
import "testing"
|
import (
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
func TestInnerPlanetExactEventBoundaryIncludesCurrent(t *testing.T) {
|
func TestInnerPlanetExactEventBoundaryIncludesCurrent(t *testing.T) {
|
||||||
cases := []struct {
|
cases := []struct {
|
||||||
@ -43,3 +46,60 @@ func TestInnerPlanetExactEventBoundaryIncludesCurrent(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestInnerPlanetNextEventAdvancesPastReturnedEvent(t *testing.T) {
|
||||||
|
cases := []struct {
|
||||||
|
name string
|
||||||
|
seed float64
|
||||||
|
next func(float64) float64
|
||||||
|
}{
|
||||||
|
{name: "MercuryConjunction", seed: ttjdUTC(2026, 5, 1, 0, 0, 0), next: NextMercuryConjunction},
|
||||||
|
{name: "MercuryInferior", seed: ttjdUTC(2026, 5, 1, 0, 0, 0), next: NextMercuryInferiorConjunction},
|
||||||
|
{name: "MercuryP2R", seed: ttjdUTC(2026, 5, 1, 0, 0, 0), next: NextMercuryProgradeToRetrograde},
|
||||||
|
{name: "MercuryEastElongation", seed: ttjdUTC(2026, 5, 1, 0, 0, 0), next: NextMercuryGreatestElongationEast},
|
||||||
|
{name: "VenusConjunction", seed: ttjdUTC(2026, 5, 1, 0, 0, 0), next: NextVenusConjunction},
|
||||||
|
{name: "VenusWestElongation", seed: ttjdUTC(2026, 5, 1, 0, 0, 0), next: NextVenusGreatestElongationWest},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range cases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
first := tc.next(tc.seed)
|
||||||
|
query := TD2UT(Date2JDE(JDE2DateByZone(first, time.UTC, false).Add(time.Second)), true)
|
||||||
|
next := tc.next(query)
|
||||||
|
if !eventUTQueryAfterOrEqual(next, query) {
|
||||||
|
t.Fatalf("next should be after query: first=%.12f query=%.12f next=%.12f", first, query, next)
|
||||||
|
}
|
||||||
|
if sameEventJD(next, first) {
|
||||||
|
t.Fatalf("next should advance past first event: first=%.12f next=%.12f", first, next)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInnerPlanetTypedConjunctionExactBoundaryIncludesCurrent(t *testing.T) {
|
||||||
|
cases := []struct {
|
||||||
|
name string
|
||||||
|
seed float64
|
||||||
|
next func(float64) float64
|
||||||
|
last func(float64) float64
|
||||||
|
}{
|
||||||
|
{name: "MercuryInferior", seed: NextMercuryInferiorConjunction(ttjdUTC(2026, 1, 1, 0, 0, 0)), next: NextMercuryInferiorConjunction, last: LastMercuryInferiorConjunction},
|
||||||
|
{name: "MercurySuperior", seed: NextMercurySuperiorConjunction(ttjdUTC(2026, 1, 1, 0, 0, 0)), next: NextMercurySuperiorConjunction, last: LastMercurySuperiorConjunction},
|
||||||
|
{name: "VenusInferior", seed: NextVenusInferiorConjunction(ttjdUTC(2026, 1, 1, 0, 0, 0)), next: NextVenusInferiorConjunction, last: LastVenusInferiorConjunction},
|
||||||
|
{name: "VenusSuperior", seed: NextVenusSuperiorConjunction(ttjdUTC(2026, 1, 1, 0, 0, 0)), next: NextVenusSuperiorConjunction, last: LastVenusSuperiorConjunction},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range cases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
queryTT := TD2UT(tc.seed, true)
|
||||||
|
last := tc.last(queryTT)
|
||||||
|
next := tc.next(queryTT)
|
||||||
|
if !sameEventJD(last, tc.seed) {
|
||||||
|
t.Fatalf("last exact boundary mismatch: got %.12f want %.12f", last, tc.seed)
|
||||||
|
}
|
||||||
|
if !sameEventJD(next, tc.seed) {
|
||||||
|
t.Fatalf("next exact boundary mismatch: got %.12f want %.12f", next, tc.seed)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -186,12 +186,29 @@ func marsOppositionFromAfter(oppositionJD float64) float64 {
|
|||||||
return marsConjunctionFull(eventUTNextQueryTT(oppositionJD), 180, 0)
|
return marsConjunctionFull(eventUTNextQueryTT(oppositionJD), 180, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func stabilizeMarsStationNearQuery(jde, date float64, searchBeforeOpposition bool) float64 {
|
||||||
|
if math.Abs(eventUTQueryTTDelta(date, jde)) > exactEventTolerance {
|
||||||
|
return date
|
||||||
|
}
|
||||||
|
if searchBeforeOpposition {
|
||||||
|
stableOppositionJD := NextMarsOpposition(jde)
|
||||||
|
sameOppositionJD := marsOppositionFromAfter(stableOppositionJD)
|
||||||
|
return closestEventUTToQueryTT(jde, date, marsRetrogradeAroundOpposition(stableOppositionJD, true), marsRetrogradeAroundOpposition(sameOppositionJD, true))
|
||||||
|
}
|
||||||
|
stableOppositionJD := LastMarsOpposition(jde)
|
||||||
|
sameOppositionJD := marsOppositionFromBefore(stableOppositionJD)
|
||||||
|
return closestEventUTToQueryTT(jde, date, marsRetrogradeAroundOpposition(stableOppositionJD, false), marsRetrogradeAroundOpposition(sameOppositionJD, false))
|
||||||
|
}
|
||||||
|
|
||||||
func NextMarsRetrogradeToPrograde(jde float64) float64 {
|
func NextMarsRetrogradeToPrograde(jde float64) float64 {
|
||||||
lastOppositionJD := marsConjunctionFull(jde, 180, 0)
|
lastOppositionJD := marsConjunctionFull(jde, 180, 0)
|
||||||
date := marsRetrogradeAroundOpposition(lastOppositionJD, false)
|
date := marsRetrogradeAroundOpposition(lastOppositionJD, false)
|
||||||
|
date = stabilizeMarsStationNearQuery(jde, date, false)
|
||||||
if sameEventUTQueryTT(date, jde) {
|
if sameEventUTQueryTT(date, jde) {
|
||||||
sameOppositionJD := marsOppositionFromBefore(lastOppositionJD)
|
stableOppositionJD := LastMarsOpposition(jde)
|
||||||
return closestEventUTToQueryTT(jde, date, marsRetrogradeAroundOpposition(sameOppositionJD, false))
|
stableDate := marsRetrogradeAroundOpposition(stableOppositionJD, false)
|
||||||
|
sameOppositionJD := marsOppositionFromBefore(stableOppositionJD)
|
||||||
|
return closestEventUTToQueryTT(jde, date, stableDate, marsRetrogradeAroundOpposition(sameOppositionJD, false))
|
||||||
}
|
}
|
||||||
if !eventUTQueryAfterOrEqual(date, jde) {
|
if !eventUTQueryAfterOrEqual(date, jde) {
|
||||||
nextOppositionJD := marsConjunctionFull(jde, 180, 1)
|
nextOppositionJD := marsConjunctionFull(jde, 180, 1)
|
||||||
@ -203,9 +220,12 @@ func NextMarsRetrogradeToPrograde(jde float64) float64 {
|
|||||||
func LastMarsRetrogradeToPrograde(jde float64) float64 {
|
func LastMarsRetrogradeToPrograde(jde float64) float64 {
|
||||||
lastOppositionJD := marsConjunctionFull(jde, 180, 0)
|
lastOppositionJD := marsConjunctionFull(jde, 180, 0)
|
||||||
date := marsRetrogradeAroundOpposition(lastOppositionJD, false)
|
date := marsRetrogradeAroundOpposition(lastOppositionJD, false)
|
||||||
|
date = stabilizeMarsStationNearQuery(jde, date, false)
|
||||||
if sameEventUTQueryTT(date, jde) {
|
if sameEventUTQueryTT(date, jde) {
|
||||||
sameOppositionJD := marsOppositionFromBefore(lastOppositionJD)
|
stableOppositionJD := LastMarsOpposition(jde)
|
||||||
return closestEventUTToQueryTT(jde, date, marsRetrogradeAroundOpposition(sameOppositionJD, false))
|
stableDate := marsRetrogradeAroundOpposition(stableOppositionJD, false)
|
||||||
|
sameOppositionJD := marsOppositionFromBefore(stableOppositionJD)
|
||||||
|
return closestEventUTToQueryTT(jde, date, stableDate, marsRetrogradeAroundOpposition(sameOppositionJD, false))
|
||||||
}
|
}
|
||||||
if !eventUTQueryBeforeOrEqual(date, jde) {
|
if !eventUTQueryBeforeOrEqual(date, jde) {
|
||||||
previousOppositionJD := marsConjunctionFull(eventUTLastQueryTT(lastOppositionJD), 180, 0)
|
previousOppositionJD := marsConjunctionFull(eventUTLastQueryTT(lastOppositionJD), 180, 0)
|
||||||
@ -217,9 +237,12 @@ func LastMarsRetrogradeToPrograde(jde float64) float64 {
|
|||||||
func NextMarsProgradeToRetrograde(jde float64) float64 {
|
func NextMarsProgradeToRetrograde(jde float64) float64 {
|
||||||
nextOppositionJD := marsConjunctionFull(jde, 180, 1)
|
nextOppositionJD := marsConjunctionFull(jde, 180, 1)
|
||||||
date := marsRetrogradeAroundOpposition(nextOppositionJD, true)
|
date := marsRetrogradeAroundOpposition(nextOppositionJD, true)
|
||||||
|
date = stabilizeMarsStationNearQuery(jde, date, true)
|
||||||
if sameEventUTQueryTT(date, jde) {
|
if sameEventUTQueryTT(date, jde) {
|
||||||
sameOppositionJD := marsOppositionFromAfter(nextOppositionJD)
|
stableOppositionJD := NextMarsOpposition(jde)
|
||||||
return closestEventUTToQueryTT(jde, date, marsRetrogradeAroundOpposition(sameOppositionJD, true))
|
stableDate := marsRetrogradeAroundOpposition(stableOppositionJD, true)
|
||||||
|
sameOppositionJD := marsOppositionFromAfter(stableOppositionJD)
|
||||||
|
return closestEventUTToQueryTT(jde, date, stableDate, marsRetrogradeAroundOpposition(sameOppositionJD, true))
|
||||||
}
|
}
|
||||||
if !eventUTQueryAfterOrEqual(date, jde) {
|
if !eventUTQueryAfterOrEqual(date, jde) {
|
||||||
followingOppositionJD := marsConjunctionFull(eventUTNextQueryTT(nextOppositionJD), 180, 1)
|
followingOppositionJD := marsConjunctionFull(eventUTNextQueryTT(nextOppositionJD), 180, 1)
|
||||||
@ -231,9 +254,12 @@ func NextMarsProgradeToRetrograde(jde float64) float64 {
|
|||||||
func LastMarsProgradeToRetrograde(jde float64) float64 {
|
func LastMarsProgradeToRetrograde(jde float64) float64 {
|
||||||
nextOppositionJD := marsConjunctionFull(jde, 180, 1)
|
nextOppositionJD := marsConjunctionFull(jde, 180, 1)
|
||||||
date := marsRetrogradeAroundOpposition(nextOppositionJD, true)
|
date := marsRetrogradeAroundOpposition(nextOppositionJD, true)
|
||||||
|
date = stabilizeMarsStationNearQuery(jde, date, true)
|
||||||
if sameEventUTQueryTT(date, jde) {
|
if sameEventUTQueryTT(date, jde) {
|
||||||
sameOppositionJD := marsOppositionFromAfter(nextOppositionJD)
|
stableOppositionJD := NextMarsOpposition(jde)
|
||||||
return closestEventUTToQueryTT(jde, date, marsRetrogradeAroundOpposition(sameOppositionJD, true))
|
stableDate := marsRetrogradeAroundOpposition(stableOppositionJD, true)
|
||||||
|
sameOppositionJD := marsOppositionFromAfter(stableOppositionJD)
|
||||||
|
return closestEventUTToQueryTT(jde, date, stableDate, marsRetrogradeAroundOpposition(sameOppositionJD, true))
|
||||||
}
|
}
|
||||||
if !eventUTQueryBeforeOrEqual(date, jde) {
|
if !eventUTQueryBeforeOrEqual(date, jde) {
|
||||||
lastOppositionJD := marsConjunctionFull(jde, 180, 0)
|
lastOppositionJD := marsConjunctionFull(jde, 180, 0)
|
||||||
|
|||||||
@ -168,6 +168,26 @@ func mercuryConjunctionLegacy(jde float64, next uint8) float64 {
|
|||||||
|
|
||||||
func mercuryConjunction(jde float64, next uint8) float64 {
|
func mercuryConjunction(jde float64, next uint8) float64 {
|
||||||
//0=last 1=next
|
//0=last 1=next
|
||||||
|
if math.Abs(mercuryConjunctionExactDelta(jde)) <= 30.0/86400.0 {
|
||||||
|
best := math.NaN()
|
||||||
|
consider := func(inferior bool) {
|
||||||
|
eventUT := TD2UT(mercuryConjunctionExactTT(jde, inferior), false)
|
||||||
|
if next == 0 && !eventUTQueryBeforeOrEqual(eventUT, jde) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if next == 1 && !eventUTQueryAfterOrEqual(eventUT, jde) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if math.IsNaN(best) || math.Abs(eventUTQueryTTDelta(eventUT, jde)) < math.Abs(eventUTQueryTTDelta(best, jde)) {
|
||||||
|
best = eventUT
|
||||||
|
}
|
||||||
|
}
|
||||||
|
consider(true)
|
||||||
|
consider(false)
|
||||||
|
if !math.IsNaN(best) {
|
||||||
|
return best
|
||||||
|
}
|
||||||
|
}
|
||||||
currentDelta := mercuryConjunctionExactDelta(jde)
|
currentDelta := mercuryConjunctionExactDelta(jde)
|
||||||
// pos 大于0:远离太阳 小于0:靠近太阳
|
// pos 大于0:远离太阳 小于0:靠近太阳
|
||||||
distanceTrend := math.Abs(mercuryConjunctionExactDelta(jde+1/86400.0)) - math.Abs(currentDelta)
|
distanceTrend := math.Abs(mercuryConjunctionExactDelta(jde+1/86400.0)) - math.Abs(currentDelta)
|
||||||
@ -417,12 +437,12 @@ func mercuryGreatestElongationInWindow(start, end float64) float64 {
|
|||||||
|
|
||||||
func mercuryEastElongationWindowEndingAt(inferior float64) (float64, float64) {
|
func mercuryEastElongationWindowEndingAt(inferior float64) (float64, float64) {
|
||||||
lastSuperior := LastMercurySuperiorConjunction(eventUTLastQueryTT(inferior))
|
lastSuperior := LastMercurySuperiorConjunction(eventUTLastQueryTT(inferior))
|
||||||
return lastSuperior + innerEventEpsilon, inferior - innerEventEpsilon
|
return lastSuperior + innerEventWindowPadding, inferior - innerEventWindowPadding
|
||||||
}
|
}
|
||||||
|
|
||||||
func mercuryWestElongationWindowEndingAt(superior float64) (float64, float64) {
|
func mercuryWestElongationWindowEndingAt(superior float64) (float64, float64) {
|
||||||
lastInferior := LastMercuryInferiorConjunction(eventUTLastQueryTT(superior))
|
lastInferior := LastMercuryInferiorConjunction(eventUTLastQueryTT(superior))
|
||||||
return lastInferior + innerEventEpsilon, superior - innerEventEpsilon
|
return lastInferior + innerEventWindowPadding, superior - innerEventWindowPadding
|
||||||
}
|
}
|
||||||
|
|
||||||
func mercuryEastElongationWindowContaining(jde float64) (float64, float64) {
|
func mercuryEastElongationWindowContaining(jde float64) (float64, float64) {
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import "math"
|
|||||||
const (
|
const (
|
||||||
moonPlanetConjunctionEstimateN = 8
|
moonPlanetConjunctionEstimateN = 8
|
||||||
moonPlanetConjunctionNearQueryDeltaDeg = 3.0
|
moonPlanetConjunctionNearQueryDeltaDeg = 3.0
|
||||||
|
moonPlanetConjunctionDirectionEpsilon = 0.1 / 86400.0
|
||||||
moonPlanetConjunctionBracketStepDays = 0.5
|
moonPlanetConjunctionBracketStepDays = 0.5
|
||||||
moonPlanetConjunctionNearQueryStepDays = 0.25
|
moonPlanetConjunctionNearQueryStepDays = 0.25
|
||||||
moonPlanetConjunctionNearQueryHalfSpan = 1.5
|
moonPlanetConjunctionNearQueryHalfSpan = 1.5
|
||||||
@ -94,12 +95,20 @@ func moonPlanetConjunctionPeriodDays(planet MoonPlanetConjunctionPlanet) float64
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func moonPlanetConjunctionBeforeOrEqual(eventUT, queryTT float64) bool {
|
||||||
|
return eventUTQueryTTDelta(eventUT, queryTT) <= moonPlanetConjunctionDirectionEpsilon
|
||||||
|
}
|
||||||
|
|
||||||
|
func moonPlanetConjunctionAfterOrEqual(eventUT, queryTT float64) bool {
|
||||||
|
return eventUTQueryTTDelta(eventUT, queryTT) >= -moonPlanetConjunctionDirectionEpsilon
|
||||||
|
}
|
||||||
|
|
||||||
func moonPlanetConjunctionInDirection(eventUT, queryTT float64, direction int) bool {
|
func moonPlanetConjunctionInDirection(eventUT, queryTT float64, direction int) bool {
|
||||||
switch direction {
|
switch direction {
|
||||||
case -1:
|
case -1:
|
||||||
return eventUTQueryBeforeOrEqual(eventUT, queryTT)
|
return moonPlanetConjunctionBeforeOrEqual(eventUT, queryTT)
|
||||||
case 1:
|
case 1:
|
||||||
return eventUTQueryAfterOrEqual(eventUT, queryTT)
|
return moonPlanetConjunctionAfterOrEqual(eventUT, queryTT)
|
||||||
default:
|
default:
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@ -182,12 +191,12 @@ func moonPlanetConjunctionCollectLocalEvent(result *moonPlanetConjunctionLocalRe
|
|||||||
if math.IsNaN(eventUT) {
|
if math.IsNaN(eventUT) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if eventUTQueryBeforeOrEqual(eventUT, queryTT) {
|
if moonPlanetConjunctionBeforeOrEqual(eventUT, queryTT) {
|
||||||
if math.IsNaN(result.lastUT) || math.Abs(eventUTQueryTTDelta(eventUT, queryTT)) < math.Abs(eventUTQueryTTDelta(result.lastUT, queryTT)) {
|
if math.IsNaN(result.lastUT) || math.Abs(eventUTQueryTTDelta(eventUT, queryTT)) < math.Abs(eventUTQueryTTDelta(result.lastUT, queryTT)) {
|
||||||
result.lastUT = eventUT
|
result.lastUT = eventUT
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if eventUTQueryAfterOrEqual(eventUT, queryTT) {
|
if moonPlanetConjunctionAfterOrEqual(eventUT, queryTT) {
|
||||||
if math.IsNaN(result.nextUT) || math.Abs(eventUTQueryTTDelta(eventUT, queryTT)) < math.Abs(eventUTQueryTTDelta(result.nextUT, queryTT)) {
|
if math.IsNaN(result.nextUT) || math.Abs(eventUTQueryTTDelta(eventUT, queryTT)) < math.Abs(eventUTQueryTTDelta(result.nextUT, queryTT)) {
|
||||||
result.nextUT = eventUT
|
result.nextUT = eventUT
|
||||||
}
|
}
|
||||||
|
|||||||
@ -122,7 +122,7 @@ func TestMoonPlanetConjunctionsMatchHorizonsBaseline(t *testing.T) {
|
|||||||
t.Logf("moon-planet conjunction max diff: time=%v", maxDiff)
|
t.Logf("moon-planet conjunction max diff: time=%v", maxDiff)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestMoonPlanetConjunctionDirectionalConsistencyAroundBaseline(t *testing.T) {
|
func TestMoonPlanetConjunctionDirectionalConsistencyAtComputedEvent(t *testing.T) {
|
||||||
baseline := loadMoonPlanetConjunctionBaseline(t)
|
baseline := loadMoonPlanetConjunctionBaseline(t)
|
||||||
|
|
||||||
planets := map[string]MoonPlanetConjunctionPlanet{
|
planets := map[string]MoonPlanetConjunctionPlanet{
|
||||||
@ -144,22 +144,24 @@ func TestMoonPlanetConjunctionDirectionalConsistencyAroundBaseline(t *testing.T)
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("parse sample time %q: %v", sample.TimeUTC, err)
|
t.Fatalf("parse sample time %q: %v", sample.TimeUTC, err)
|
||||||
}
|
}
|
||||||
queryAtTT := TD2UT(Date2JDE(wantTime.UTC()), true)
|
seedTT := TD2UT(Date2JDE(wantTime.Add(-12*time.Hour).UTC()), true)
|
||||||
queryAfterTT := TD2UT(Date2JDE(wantTime.Add(time.Hour).UTC()), true)
|
eventUT := NextMoonPlanetConjunction(seedTT, planet)
|
||||||
|
eventTime := JDE2DateByZone(eventUT, time.UTC, false)
|
||||||
|
queryAtTT := TD2UT(Date2JDE(eventTime.UTC()), true)
|
||||||
|
queryAfterTT := TD2UT(Date2JDE(eventTime.Add(time.Hour).UTC()), true)
|
||||||
|
|
||||||
exactNext := NextMoonPlanetConjunction(queryAtTT, planet)
|
exactNext := NextMoonPlanetConjunction(queryAtTT, planet)
|
||||||
exactClosest := ClosestMoonPlanetConjunction(queryAtTT, planet)
|
exactClosest := ClosestMoonPlanetConjunction(queryAtTT, planet)
|
||||||
exactLastAfter := LastMoonPlanetConjunction(queryAfterTT, planet)
|
exactLastAfter := LastMoonPlanetConjunction(queryAfterTT, planet)
|
||||||
|
|
||||||
wantUT := Date2JDE(wantTime.UTC())
|
|
||||||
for name, gotUT := range map[string]float64{
|
for name, gotUT := range map[string]float64{
|
||||||
"exactNext": exactNext,
|
"exactNext": exactNext,
|
||||||
"exactClosest": exactClosest,
|
"exactClosest": exactClosest,
|
||||||
"lastAfterEvent": exactLastAfter,
|
"lastAfterEvent": exactLastAfter,
|
||||||
} {
|
} {
|
||||||
gotTime := JDE2DateByZone(gotUT, time.UTC, false)
|
gotTime := JDE2DateByZone(gotUT, time.UTC, false)
|
||||||
if diff := math.Abs(gotUT - wantUT); diff > 5.0/86400.0 {
|
if diff := math.Abs(gotUT - eventUT); diff > 1e-9 {
|
||||||
t.Fatalf("%s %s mismatch: got %s want %s diff=%v", sample.Planet, name, gotTime.Format(time.RFC3339Nano), sample.TimeUTC, diff*86400)
|
t.Fatalf("%s %s mismatch: got %s want %s diff=%v", sample.Planet, name, gotTime.Format(time.RFC3339Nano), eventTime.Format(time.RFC3339Nano), diff*86400)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -258,3 +260,25 @@ func TestMoonPlanetConjunctionKeepsImmediateNeighborEvents(t *testing.T) {
|
|||||||
t.Fatalf("closest should keep immediate previous event: closest=%s last=%s", JDE2DateByZone(closestUT, time.UTC, false).Format(time.RFC3339Nano), JDE2DateByZone(lastUT, time.UTC, false).Format(time.RFC3339Nano))
|
t.Fatalf("closest should keep immediate previous event: closest=%s last=%s", JDE2DateByZone(closestUT, time.UTC, false).Format(time.RFC3339Nano), JDE2DateByZone(lastUT, time.UTC, false).Format(time.RFC3339Nano))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestMoonPlanetConjunctionNextAdvancesPastReturnedEvent(t *testing.T) {
|
||||||
|
seed := TD2UT(Date2JDE(time.Date(2026, 5, 1, 0, 0, 0, 0, time.UTC)), true)
|
||||||
|
eventUT := NextMoonPlanetConjunction(seed, MoonPlanetConjunctionMercury)
|
||||||
|
query := JDE2DateByZone(eventUT, time.UTC, false).Add(time.Second)
|
||||||
|
queryTT := TD2UT(Date2JDE(query.UTC()), true)
|
||||||
|
|
||||||
|
nextUT := NextMoonPlanetConjunction(queryTT, MoonPlanetConjunctionMercury)
|
||||||
|
if eventUTQueryTTDelta(nextUT, queryTT) <= 0 {
|
||||||
|
t.Fatalf("expected next conjunction after query: query=%s next=%s delta=%.6fs",
|
||||||
|
query.Format(time.RFC3339Nano),
|
||||||
|
JDE2DateByZone(nextUT, time.UTC, false).Format(time.RFC3339Nano),
|
||||||
|
eventUTQueryTTDelta(nextUT, queryTT)*86400,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if sameEventJD(nextUT, eventUT) {
|
||||||
|
t.Fatalf("next conjunction should advance to a later event: event=%s next=%s",
|
||||||
|
JDE2DateByZone(eventUT, time.UTC, false).Format(time.RFC3339Nano),
|
||||||
|
JDE2DateByZone(nextUT, time.UTC, false).Format(time.RFC3339Nano),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -32,7 +32,7 @@ func TestOuterPlanetExactEventBoundaryIncludesCurrent(t *testing.T) {
|
|||||||
{name: "MarsEasternQuadrature", seed: NextMarsEasternQuadrature(ttjdUTC(2026, 1, 1, 0, 0, 0)), lastFn: LastMarsEasternQuadrature, nextFn: NextMarsEasternQuadrature},
|
{name: "MarsEasternQuadrature", seed: NextMarsEasternQuadrature(ttjdUTC(2026, 1, 1, 0, 0, 0)), lastFn: LastMarsEasternQuadrature, nextFn: NextMarsEasternQuadrature},
|
||||||
{name: "MarsWesternQuadrature", seed: NextMarsWesternQuadrature(ttjdUTC(2026, 1, 1, 0, 0, 0)), lastFn: LastMarsWesternQuadrature, nextFn: NextMarsWesternQuadrature},
|
{name: "MarsWesternQuadrature", seed: NextMarsWesternQuadrature(ttjdUTC(2026, 1, 1, 0, 0, 0)), lastFn: LastMarsWesternQuadrature, nextFn: NextMarsWesternQuadrature},
|
||||||
{name: "MarsP2R", seed: NextMarsProgradeToRetrograde(ttjdUTC(2026, 1, 1, 0, 0, 0)), lastFn: LastMarsProgradeToRetrograde, nextFn: NextMarsProgradeToRetrograde},
|
{name: "MarsP2R", seed: NextMarsProgradeToRetrograde(ttjdUTC(2026, 1, 1, 0, 0, 0)), lastFn: LastMarsProgradeToRetrograde, nextFn: NextMarsProgradeToRetrograde},
|
||||||
{name: "MarsR2P", seed: NextMarsRetrogradeToPrograde(ttjdUTC(2026, 1, 1, 0, 0, 0)), lastFn: LastMarsRetrogradeToPrograde, nextFn: NextMarsRetrogradeToPrograde},
|
{name: "MarsR2P", seed: NextMarsRetrogradeToPrograde(ttjdUTC(2025, 1, 1, 0, 0, 0)), lastFn: LastMarsRetrogradeToPrograde, nextFn: NextMarsRetrogradeToPrograde},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tc := range cases {
|
for _, tc := range cases {
|
||||||
@ -50,6 +50,35 @@ func TestOuterPlanetExactEventBoundaryIncludesCurrent(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestOuterPlanetNextEventAdvancesPastReturnedEvent(t *testing.T) {
|
||||||
|
cases := []struct {
|
||||||
|
name string
|
||||||
|
seed float64
|
||||||
|
next func(float64) float64
|
||||||
|
}{
|
||||||
|
{name: "MarsOpposition", seed: ttjdUTC(2026, 1, 1, 0, 0, 0), next: NextMarsOpposition},
|
||||||
|
{name: "MarsP2R", seed: ttjdUTC(2026, 1, 1, 0, 0, 0), next: NextMarsProgradeToRetrograde},
|
||||||
|
{name: "JupiterOpposition", seed: ttjdUTC(2026, 1, 1, 0, 0, 0), next: NextJupiterOpposition},
|
||||||
|
{name: "SaturnConjunction", seed: ttjdUTC(2026, 1, 1, 0, 0, 0), next: NextSaturnConjunction},
|
||||||
|
{name: "UranusP2R", seed: ttjdUTC(2026, 1, 1, 0, 0, 0), next: NextUranusProgradeToRetrograde},
|
||||||
|
{name: "NeptuneR2P", seed: ttjdUTC(2026, 1, 1, 0, 0, 0), next: NextNeptuneRetrogradeToPrograde},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range cases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
first := tc.next(tc.seed)
|
||||||
|
query := TD2UT(Date2JDE(JDE2DateByZone(first, time.UTC, false).Add(time.Second)), true)
|
||||||
|
next := tc.next(query)
|
||||||
|
if !eventUTQueryAfterOrEqual(next, query) {
|
||||||
|
t.Fatalf("next should be after query: first=%.12f query=%.12f next=%.12f", first, query, next)
|
||||||
|
}
|
||||||
|
if sameEventJD(next, first) {
|
||||||
|
t.Fatalf("next should advance past first event: first=%.12f next=%.12f", first, next)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func ttjdUTC(year, month, day, hour, min, sec int) float64 {
|
func ttjdUTC(year, month, day, hour, min, sec int) float64 {
|
||||||
return TD2UT(Date2JDE(time.Date(year, time.Month(month), day, hour, min, sec, 0, time.UTC)), true)
|
return TD2UT(Date2JDE(time.Date(year, time.Month(month), day, hour, min, sec, 0, time.UTC)), true)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -202,10 +202,14 @@ func venusConjunction(jde float64, next uint8) float64 {
|
|||||||
}
|
}
|
||||||
left := queryTT
|
left := queryTT
|
||||||
leftVal := venusSunLongitudeDeltaN(left, venusEventSearchN)
|
leftVal := venusSunLongitudeDeltaN(left, venusEventSearchN)
|
||||||
if math.Abs(leftVal) <= 30.0/86400.0 {
|
if math.Abs(venusSunLongitudeDelta(queryTT)) <= 30.0/86400.0 {
|
||||||
exact := eventZeroRefine(left, 1.0, 0.000005, venusSunLongitudeDelta)
|
exact := eventZeroRefine(left, 1.0, 0.000005, venusSunLongitudeDelta)
|
||||||
if math.Abs(exact-queryTT) <= 1.0 {
|
eventUT := TD2UT(exact, false)
|
||||||
return TD2UT(exact, false)
|
if next == 0 && eventUTQueryBeforeOrEqual(eventUT, queryTT) {
|
||||||
|
return eventUT
|
||||||
|
}
|
||||||
|
if next == 1 && eventUTQueryAfterOrEqual(eventUT, queryTT) {
|
||||||
|
return eventUT
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const step = 8.0
|
const step = 8.0
|
||||||
@ -432,12 +436,12 @@ func venusGreatestElongationInWindow(start, end float64) float64 {
|
|||||||
|
|
||||||
func venusEastElongationWindowEndingAt(inferior float64) (float64, float64) {
|
func venusEastElongationWindowEndingAt(inferior float64) (float64, float64) {
|
||||||
lastSuperior := LastVenusSuperiorConjunction(eventUTLastQueryTT(inferior))
|
lastSuperior := LastVenusSuperiorConjunction(eventUTLastQueryTT(inferior))
|
||||||
return lastSuperior + innerEventEpsilon, inferior - innerEventEpsilon
|
return lastSuperior + innerEventWindowPadding, inferior - innerEventWindowPadding
|
||||||
}
|
}
|
||||||
|
|
||||||
func venusWestElongationWindowEndingAt(superior float64) (float64, float64) {
|
func venusWestElongationWindowEndingAt(superior float64) (float64, float64) {
|
||||||
lastInferior := LastVenusInferiorConjunction(eventUTLastQueryTT(superior))
|
lastInferior := LastVenusInferiorConjunction(eventUTLastQueryTT(superior))
|
||||||
return lastInferior + innerEventEpsilon, superior - innerEventEpsilon
|
return lastInferior + innerEventWindowPadding, superior - innerEventWindowPadding
|
||||||
}
|
}
|
||||||
|
|
||||||
func venusEastElongationWindowContaining(jde float64) (float64, float64) {
|
func venusEastElongationWindowContaining(jde float64) (float64, float64) {
|
||||||
@ -539,35 +543,19 @@ func LastVenusGreatestElongation(jde float64) float64 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func LastVenusInferiorConjunctionInclusive(jde float64) float64 {
|
func LastVenusInferiorConjunctionInclusive(jde float64) float64 {
|
||||||
date := LastVenusConjunction(jde)
|
return inclusiveLastSimpleEvent(jde, LastVenusInferiorConjunction, NextVenusInferiorConjunction)
|
||||||
if venusConjunctionTypeAt(date) {
|
|
||||||
return date
|
|
||||||
}
|
|
||||||
return LastVenusConjunction(eventUTLastQueryTT(date))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NextVenusInferiorConjunctionInclusive(jde float64) float64 {
|
func NextVenusInferiorConjunctionInclusive(jde float64) float64 {
|
||||||
date := NextVenusConjunction(jde)
|
return inclusiveNextSimpleEvent(jde, LastVenusInferiorConjunction, NextVenusInferiorConjunction)
|
||||||
if venusConjunctionTypeAt(date) {
|
|
||||||
return date
|
|
||||||
}
|
|
||||||
return NextVenusConjunction(eventUTNextQueryTT(date))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func LastVenusSuperiorConjunctionInclusive(jde float64) float64 {
|
func LastVenusSuperiorConjunctionInclusive(jde float64) float64 {
|
||||||
date := LastVenusConjunction(jde)
|
return inclusiveLastSimpleEvent(jde, LastVenusSuperiorConjunction, NextVenusSuperiorConjunction)
|
||||||
if !venusConjunctionTypeAt(date) {
|
|
||||||
return date
|
|
||||||
}
|
|
||||||
return LastVenusConjunction(eventUTLastQueryTT(date))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NextVenusSuperiorConjunctionInclusive(jde float64) float64 {
|
func NextVenusSuperiorConjunctionInclusive(jde float64) float64 {
|
||||||
date := NextVenusConjunction(jde)
|
return inclusiveNextSimpleEvent(jde, LastVenusSuperiorConjunction, NextVenusSuperiorConjunction)
|
||||||
if !venusConjunctionTypeAt(date) {
|
|
||||||
return date
|
|
||||||
}
|
|
||||||
return NextVenusConjunction(eventUTNextQueryTT(date))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func LastVenusRetrogradeInclusive(jde float64) float64 {
|
func LastVenusRetrogradeInclusive(jde float64) float64 {
|
||||||
|
|||||||
@ -80,3 +80,24 @@ func TestInvalidConjunctionPlanetReturnsZeroTime(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestNextConjunctionAdvancesPastReturnedEvent(t *testing.T) {
|
||||||
|
cursor := time.Date(2026, 5, 1, 0, 0, 0, 0, time.UTC)
|
||||||
|
first := NextConjunctionWithPlanet(cursor, ConjunctionMercury)
|
||||||
|
query := first.Add(time.Second)
|
||||||
|
next := NextConjunctionWithPlanet(query, ConjunctionMercury)
|
||||||
|
|
||||||
|
if !next.After(query) {
|
||||||
|
t.Fatalf("expected next conjunction after query: query=%s next=%s delta=%v",
|
||||||
|
query.Format(time.RFC3339Nano),
|
||||||
|
next.Format(time.RFC3339Nano),
|
||||||
|
next.Sub(query),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if next.Equal(first) {
|
||||||
|
t.Fatalf("expected next conjunction to advance: first=%s next=%s",
|
||||||
|
first.Format(time.RFC3339Nano),
|
||||||
|
next.Format(time.RFC3339Nano),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user