Files
astro/basic/occultation_rise_set_vector_test.go
T
b612 16c62a97d5 feat: 完善时标与天象几何计算并扩展输出接口
- 新增时标、ΔT 模型、质心时间与 UT1 支持
- 改进日月食、月掩、行星事件及路径边界计算
- 完善恒星三维自行与动态距离传播
- 扩展 SVG、GeoJSON、KML 输出与底层距离换算工具
- 整理中英文手册、示例资源及回归测试
2026-09-23 18:55:12 +08:00

245 lines
10 KiB
Go

package basic
import (
"math"
"testing"
"time"
)
func TestOccultationRiseSetVectorStateMatchesLegacyTopocentricState(t *testing.T) {
planetConfig, ok := planetOccultationConfigFor(OccultationSaturn)
if !ok {
t.Fatal("Saturn occultation config is unavailable")
}
planetTT := occultationTimeToTT(time.Date(2024, time.August, 21, 2, 41, 36, 0, time.UTC))
planetState := planetOccultationEphemerisStateAt(planetTT, planetConfig)
star := StarCoordinate{
ID: "parallax-test", RA: 247.3516666666667, Dec: -26.431944444444444,
Epoch: time.Date(2000, 1, 1, 12, 0, 0, 0, time.UTC), Frame: CoordinateFrameJ2000,
ProperMotionRACosDecMasPerYear: -10, ProperMotionDecMasPerYear: -20, ParallaxMas: 24,
}
starTT := occultationTimeToTT(time.Date(2026, time.February, 11, 12, 0, 0, 0, time.UTC))
starState := starOccultationEphemerisStateAt(starTT, star)
contexts := []struct {
name string
tt float64
moonRA, moonDec, moonDistanceKM float64
targetRA, targetDec, targetDistanceKM float64
targetRadiusKM float64
}{
{
name: "planet", tt: planetTT,
moonRA: planetState.moonRA, moonDec: planetState.moonDec, moonDistanceKM: planetState.moonDistanceKM,
targetRA: planetState.planetRA, targetDec: planetState.planetDec, targetDistanceKM: planetState.planetDistanceKM,
targetRadiusKM: planetConfig.equatorialRadiusKM,
},
{
name: "finite-distance-star", tt: starTT,
moonRA: starState.moonRA, moonDec: starState.moonDec, moonDistanceKM: starState.moonDistanceKM,
targetRA: starState.starRA, targetDec: starState.starDec, targetDistanceKM: starState.starDistanceKM,
},
{
name: "infinite-distance-star", tt: starTT,
moonRA: starState.moonRA, moonDec: starState.moonDec, moonDistanceKM: starState.moonDistanceKM,
targetRA: starState.starRA, targetDec: starState.starDec,
},
}
locations := []Observer{
{Longitude: 0, Latitude: 0},
{Longitude: 115.4, Latitude: 32.9},
{Longitude: -73.9857, Latitude: 40.7484},
{Longitude: 179.9, Latitude: 80},
{Longitude: -120, Latitude: -70},
}
for _, test := range contexts {
t.Run(test.name, func(t *testing.T) {
context := newOccultationRiseSetContext(
test.tt, test.moonRA, test.moonDec, test.moonDistanceKM,
test.targetRA, test.targetDec, test.targetDistanceKM, test.targetRadiusKM,
)
for _, observer := range locations {
got := context.stateAt(observer.Longitude, observer.Latitude)
want := legacyOccultationRiseSetStateAt(
test.tt, test.moonRA, test.moonDec, test.moonDistanceKM,
test.targetRA, test.targetDec, test.targetDistanceKM, test.targetRadiusKM,
observer.Longitude, observer.Latitude,
)
if got.valid != want.valid {
t.Fatalf("observer %.4f %.4f valid=%t, want %t", observer.Longitude, observer.Latitude, got.valid, want.valid)
}
if !got.valid {
continue
}
assertOccultationRiseSetStateClose(t, "contact metric", got.contactMetric, want.contactMetric, 2e-10)
assertOccultationRiseSetStateClose(t, "separation squared", got.separationSquared, want.separationSquared, 2e-13)
assertOccultationRiseSetStateClose(t, "moon altitude", got.moonAltitude, want.moonAltitude, 2e-10)
}
})
}
}
func TestOccultationRiseSetContextFromVectorsMatchesRADecContext(t *testing.T) {
config, ok := planetOccultationConfigFor(OccultationSaturn)
if !ok {
t.Fatal("Saturn occultation config is unavailable")
}
tt := occultationTimeToTT(time.Date(2025, time.January, 5, 17, 0, 0, 0, time.UTC))
state := planetOccultationEphemerisStateAt(tt, config)
moon := occultationPathRaDecVector(state.moonRA, state.moonDec, state.moonDistanceKM)
target := occultationPathRaDecVector(state.planetRA, state.planetDec, state.planetDistanceKM)
vectorContext := newOccultationRiseSetContextFromVectors(
tt,
[3]float64{moon.x, moon.y, moon.z},
[3]float64{target.x, target.y, target.z},
true,
config.equatorialRadiusKM,
)
raDecContext := newOccultationRiseSetContext(
tt, state.moonRA, state.moonDec, state.moonDistanceKM,
state.planetRA, state.planetDec, state.planetDistanceKM, config.equatorialRadiusKM,
)
for _, observer := range []Observer{
{Longitude: 0, Latitude: 0},
{Longitude: 115.4, Latitude: 32.9},
{Longitude: -45, Latitude: 82},
{Longitude: 170, Latitude: -70},
} {
got := vectorContext.stateAt(observer.Longitude, observer.Latitude)
want := raDecContext.stateAt(observer.Longitude, observer.Latitude)
if got.valid != want.valid {
t.Fatalf("observer %.3f %.3f valid=%t, want %t", observer.Longitude, observer.Latitude, got.valid, want.valid)
}
if !got.valid {
continue
}
assertOccultationRiseSetStateClose(t, "contact metric", got.contactMetric, want.contactMetric, 1e-12)
assertOccultationRiseSetStateClose(t, "separation squared", got.separationSquared, want.separationSquared, 1e-15)
assertOccultationRiseSetStateClose(t, "moon altitude", got.moonAltitude, want.moonAltitude, 1e-12)
}
}
func TestOccultationRiseSetMoonHorizonResidualMatchesVectorAltitude(t *testing.T) {
config, ok := planetOccultationConfigFor(OccultationSaturn)
if !ok {
t.Fatal("Saturn occultation config is unavailable")
}
tt := occultationTimeToTT(time.Date(2025, time.January, 5, 17, 0, 0, 0, time.UTC))
state := planetOccultationEphemerisStateAt(tt, config)
context := newOccultationRiseSetContext(
tt, state.moonRA, state.moonDec, state.moonDistanceKM,
state.planetRA, state.planetDec, state.planetDistanceKM, config.equatorialRadiusKM,
)
for _, observer := range []Observer{
{Longitude: 0, Latitude: 0},
{Longitude: 115.4, Latitude: 32.9},
{Longitude: -45, Latitude: 82},
{Longitude: 170, Latitude: -70},
} {
got, valid := context.moonHorizonResidual(observer.Longitude, observer.Latitude)
observerParallax, _, zenith := occultationRiseSetObserverVectors(
context.siderealDegrees, observer.Longitude, observer.Latitude,
)
topocentricMoon := occultationPathSub(context.moon.positionKM, observerParallax)
want := occultationPathDot(topocentricMoon, zenith)
if !valid || math.Abs(got-want) > 1e-8 {
t.Fatalf("observer %.3f %.3f horizon residual=%.15g valid=%t, want %.15g",
observer.Longitude, observer.Latitude, got, valid, want)
}
altitude := context.stateAt(observer.Longitude, observer.Latitude).moonAltitude * rad
if difference := math.Abs(got/occultationPathNorm(topocentricMoon) - math.Sin(altitude)); difference > 1e-14 {
t.Fatalf("observer %.3f %.3f normalized horizon residual differs by %.3g",
observer.Longitude, observer.Latitude, difference)
}
}
}
func TestOccultationRiseSetEvaluationCacheSeparatesCandidateAndExactContexts(t *testing.T) {
var exactCalls, candidateCalls int
contextAt := func(tt float64) occultationRiseSetContext {
return newOccultationRiseSetContext(tt, 10, 5, 384000, 10.5, 5.25, 1e9, 0)
}
cache := newOccultationRiseSetEvaluationCacheWithCandidate(
func(tt float64) occultationRiseSetContext {
exactCalls++
return contextAt(tt)
},
func(tt float64) occultationRiseSetContext {
candidateCalls++
return contextAt(tt)
},
)
tt := occultationTimeToTT(time.Date(2025, time.January, 5, 17, 0, 0, 0, time.UTC))
cache.candidateEvaluation(tt)
cache.candidateEvaluation(tt)
if exactCalls != 0 || candidateCalls != 3 {
t.Fatalf("candidate evaluation calls exact=%d candidate=%d, want 0/3", exactCalls, candidateCalls)
}
cache.evaluation(tt)
cache.evaluation(tt)
if exactCalls != 3 || candidateCalls != 3 {
t.Fatalf("exact evaluation calls exact=%d candidate=%d, want 3/3", exactCalls, candidateCalls)
}
}
func legacyOccultationRiseSetStateAt(
tt, moonRA, moonDec, moonDistanceKM,
targetRA, targetDec, targetDistanceKM, targetRadiusKM,
longitude, latitude float64,
) occultationRiseSetState {
siderealDegrees := ApparentSiderealTime(TT2UT1(tt)) * 15
observer := Observer{Longitude: longitude, Latitude: latitude}
moonTopocentricRA, moonTopocentricDec := topocentricRaDecWithSidereal(
moonRA, moonDec, latitude, longitude, siderealDegrees,
moonDistanceKM/occultationPathAstronomicalUnitKM, 0,
)
moonTopocentricRA = normalizeRA(moonTopocentricRA)
targetTopocentricRA, targetTopocentricDec := targetRA, targetDec
if targetDistanceKM > 0 {
targetTopocentricRA, targetTopocentricDec = topocentricRaDecWithSidereal(
targetRA, targetDec, latitude, longitude, siderealDegrees,
targetDistanceKM/occultationPathAstronomicalUnitKM, 0,
)
targetTopocentricRA = normalizeRA(targetTopocentricRA)
}
moonTopocentricDistanceKM := topocentricDistanceKMWithSidereal(
moonRA, moonDec, moonDistanceKM, observer, siderealDegrees,
)
if !finite(moonTopocentricDistanceKM) || moonTopocentricDistanceKM <= moonEquatorialRadiusKM {
return occultationRiseSetState{}
}
moonRadius := angularSemidiameterArcsec(moonEquatorialRadiusKM, moonTopocentricDistanceKM) / 3600
targetRadius := 0.0
if targetRadiusKM > 0 {
targetTopocentricDistanceKM := topocentricDistanceKMWithSidereal(
targetRA, targetDec, targetDistanceKM, observer, siderealDegrees,
)
if !finite(targetTopocentricDistanceKM) || targetTopocentricDistanceKM <= targetRadiusKM {
return occultationRiseSetState{}
}
targetRadius = angularSemidiameterArcsec(targetRadiusKM, targetTopocentricDistanceKM) / 3600
}
separation := angularSeparationDegrees(
moonTopocentricRA, moonTopocentricDec, targetTopocentricRA, targetTopocentricDec,
)
separationRad := separation * rad
moonAltitude := occultationAltitudeWithSidereal(
siderealDegrees, observer, moonTopocentricRA, moonTopocentricDec,
)
return occultationRiseSetState{
contactMetric: separation - moonRadius - targetRadius,
separationSquared: 2 - 2*math.Cos(separationRad),
moonAltitude: moonAltitude,
valid: finite(separation) && finite(moonRadius) && finite(targetRadius) && finite(moonAltitude),
}
}
func assertOccultationRiseSetStateClose(t *testing.T, name string, got, want, tolerance float64) {
t.Helper()
if difference := math.Abs(got - want); difference > tolerance {
t.Fatalf("%s=%.15g, want %.15g (difference %.3g, tolerance %.3g)", name, got, want, difference, tolerance)
}
}