16c62a97d5
- 新增时标、ΔT 模型、质心时间与 UT1 支持 - 改进日月食、月掩、行星事件及路径边界计算 - 完善恒星三维自行与动态距离传播 - 扩展 SVG、GeoJSON、KML 输出与底层距离换算工具 - 整理中英文手册、示例资源及回归测试
158 lines
6.8 KiB
Go
158 lines
6.8 KiB
Go
package basic
|
||
|
||
import (
|
||
"math"
|
||
"testing"
|
||
"time"
|
||
)
|
||
|
||
func TestSolarEclipseLocalEphemerisBoundedError(t *testing.T) {
|
||
events := []struct {
|
||
name string
|
||
seed float64
|
||
}{
|
||
{"2010-01-15", JDCalc(2010, 1, 15)},
|
||
{"2014-04-29", JDCalc(2014, 4, 29)},
|
||
{"2023-04-20", JDCalc(2023, 4, 20)},
|
||
{"2031-05-21", JDCalc(2031, 5, 21)},
|
||
{"2309-06-09", JDCalc(2309, 6, 9)},
|
||
}
|
||
for _, event := range events {
|
||
t.Run(event.name, func(t *testing.T) {
|
||
center := CalcMoonSHByJDE(event.seed, 0)
|
||
ephemeris := newSolarEclipseLocalEphemeris(center)
|
||
for offset := -4.5; offset <= 4.5; offset += 0.25 {
|
||
jd := center + offset/24
|
||
approxSun, approxMoon, ok := ephemeris.equatorialAt(jd)
|
||
if !ok {
|
||
t.Fatalf("interpolator rejected in-window time %.3fh", offset)
|
||
}
|
||
exactSun, exactMoon := solarEclipseSunMoonEquatorial(jd)
|
||
for name, pair := range map[string][2][3]float64{
|
||
"sun": {solarEclipseLLRToXYZ(approxSun[0], approxSun[1], approxSun[2]), solarEclipseLLRToXYZ(exactSun[0], exactSun[1], exactSun[2])},
|
||
"moon": {solarEclipseLLRToXYZ(approxMoon[0], approxMoon[1], approxMoon[2]), solarEclipseLLRToXYZ(exactMoon[0], exactMoon[1], exactMoon[2])},
|
||
} {
|
||
if errorKM := vectorDifferenceKM(pair[0], pair[1]); errorKM > 1 {
|
||
t.Fatalf("%s interpolation error at %.3fh = %.6f km", name, offset, errorKM)
|
||
}
|
||
}
|
||
}
|
||
if _, _, ok := ephemeris.equatorialAt(center + 6.1/24); ok {
|
||
t.Fatal("interpolator should reject times outside its bounded window")
|
||
}
|
||
})
|
||
}
|
||
}
|
||
|
||
func TestOccultationLocalEphemerisBoundedError(t *testing.T) {
|
||
star := hr4799OccultationCoordinateForTest()
|
||
starTT := occultationTimeToTT(time.Date(2025, 6, 5, 20, 0, 0, 0, time.UTC))
|
||
starEphemeris := newStarOccultationLocalEphemeris(starTT, star)
|
||
for offset := -4.5; offset <= 4.5; offset += 0.25 {
|
||
jd := starTT + offset/24
|
||
got, ok := starEphemeris.stateAt(jd)
|
||
if !ok {
|
||
t.Fatalf("star interpolator rejected in-window time %.3fh", offset)
|
||
}
|
||
want := starOccultationEphemerisStateAt(jd, star)
|
||
if difference := angularSeparationDegrees(got.moonRA, got.moonDec, want.moonRA, want.moonDec); difference > 1e-5 {
|
||
t.Fatalf("moon direction interpolation error at %.3fh = %.9f deg", offset, difference)
|
||
}
|
||
if difference := angularSeparationDegrees(got.starRA, got.starDec, want.starRA, want.starDec); difference > 1e-5 {
|
||
t.Fatalf("star direction interpolation error at %.3fh = %.9f deg", offset, difference)
|
||
}
|
||
}
|
||
|
||
config, ok := planetOccultationConfigFor(OccultationSaturn)
|
||
if !ok {
|
||
t.Fatal("Saturn occultation config is unavailable")
|
||
}
|
||
planetTT := occultationTimeToTT(time.Date(2024, 8, 21, 2, 40, 0, 0, time.UTC))
|
||
planetEphemeris := newPlanetOccultationLocalEphemeris(planetTT, config)
|
||
for offset := -4.5; offset <= 4.5; offset += 0.25 {
|
||
jd := planetTT + offset/24
|
||
got, ok := planetEphemeris.stateAt(jd)
|
||
if !ok {
|
||
t.Fatalf("planet interpolator rejected in-window time %.3fh", offset)
|
||
}
|
||
want := planetOccultationEphemerisStateAt(jd, config)
|
||
if difference := angularSeparationDegrees(got.moonRA, got.moonDec, want.moonRA, want.moonDec); difference > 1e-5 {
|
||
t.Fatalf("moon direction interpolation error at %.3fh = %.9f deg", offset, difference)
|
||
}
|
||
if difference := angularSeparationDegrees(got.planetRA, got.planetDec, want.planetRA, want.planetDec); difference > 1e-5 {
|
||
t.Fatalf("planet direction interpolation error at %.3fh = %.9f deg", offset, difference)
|
||
}
|
||
if math.Abs(got.planetDistanceKM-want.planetDistanceKM) > 100 {
|
||
t.Fatalf("planet distance interpolation error at %.3fh = %.6f km", offset, math.Abs(got.planetDistanceKM-want.planetDistanceKM))
|
||
}
|
||
}
|
||
}
|
||
|
||
func TestOccultationLocalEphemerisFullWindowBoundedError(t *testing.T) {
|
||
star := hr4799OccultationCoordinateForTest()
|
||
starTT := occultationTimeToTT(time.Date(2025, 6, 5, 20, 0, 0, 0, time.UTC))
|
||
starEphemeris := newStarOccultationLocalEphemeris(starTT, star)
|
||
config, ok := planetOccultationConfigFor(OccultationSaturn)
|
||
if !ok {
|
||
t.Fatal("Saturn occultation config is unavailable")
|
||
}
|
||
planetTT := occultationTimeToTT(time.Date(2024, 8, 21, 2, 40, 0, 0, time.UTC))
|
||
planetEphemeris := newPlanetOccultationLocalEphemeris(planetTT, config)
|
||
for _, offset := range []float64{-47.5, -40, -24, 24, 40, 47.5} {
|
||
starJD := starTT + offset/24
|
||
gotStar, starOK := starEphemeris.stateAt(starJD)
|
||
wantStar := starOccultationEphemerisStateAt(starJD, star)
|
||
if !starOK {
|
||
t.Fatalf("star interpolator rejected in-window time %.3fh", offset)
|
||
}
|
||
if difference := angularSeparationDegrees(gotStar.moonRA, gotStar.moonDec, wantStar.moonRA, wantStar.moonDec); difference > 1e-5 {
|
||
t.Fatalf("star-event moon direction error at %.3fh = %.9f deg", offset, difference)
|
||
}
|
||
if difference := angularSeparationDegrees(gotStar.starRA, gotStar.starDec, wantStar.starRA, wantStar.starDec); difference > 1e-5 {
|
||
t.Fatalf("star direction error at %.3fh = %.9f deg", offset, difference)
|
||
}
|
||
|
||
planetJD := planetTT + offset/24
|
||
gotPlanet, planetOK := planetEphemeris.stateAt(planetJD)
|
||
wantPlanet := planetOccultationEphemerisStateAt(planetJD, config)
|
||
if !planetOK {
|
||
t.Fatalf("planet interpolator rejected in-window time %.3fh", offset)
|
||
}
|
||
if difference := angularSeparationDegrees(gotPlanet.moonRA, gotPlanet.moonDec, wantPlanet.moonRA, wantPlanet.moonDec); difference > 1e-5 {
|
||
t.Fatalf("planet-event moon direction error at %.3fh = %.9f deg", offset, difference)
|
||
}
|
||
if difference := angularSeparationDegrees(gotPlanet.planetRA, gotPlanet.planetDec, wantPlanet.planetRA, wantPlanet.planetDec); difference > 1e-5 {
|
||
t.Fatalf("planet direction error at %.3fh = %.9f deg", offset, difference)
|
||
}
|
||
if difference := math.Abs(gotPlanet.planetDistanceKM - wantPlanet.planetDistanceKM); difference > 100 {
|
||
t.Fatalf("planet distance error at %.3fh = %.6f km", offset, difference)
|
||
}
|
||
}
|
||
}
|
||
|
||
func vectorDifferenceKM(a, b [3]float64) float64 {
|
||
return math.Sqrt((a[0]-b[0])*(a[0]-b[0]) + (a[1]-b[1])*(a[1]-b[1]) + (a[2]-b[2])*(a[2]-b[2]))
|
||
}
|
||
|
||
// 矢量回读的赤经必须落在 [0,360):atan2 给 (−180,180],而精确分支
|
||
// (LoBoToRaDec、starMeanToApparentRaDec)与 rise/set 的矢量回读都在 [0,360),
|
||
// 不归一会让同一时刻的密集/精确状态相差 360 度,误导比较与日志。
|
||
func TestOccultationPathVectorRaDecKeepsNormalizedRA(t *testing.T) {
|
||
for _, want := range []float64{0.5, 90, 189.1975, 270, 359.9} {
|
||
vector := occultationPathRaDecVector(want, -5.8, 2.5)
|
||
got, dec, distance, ok := occultationPathVectorRaDec(vector)
|
||
if !ok {
|
||
t.Fatalf("RA %.4f 的回读失败", want)
|
||
}
|
||
if math.Abs(got-want) > 1e-9 {
|
||
t.Fatalf("RA %.4f 回读为 %.4f,want [0,360) 内的同一读数", want, got)
|
||
}
|
||
if math.Abs(dec+5.8) > 1e-9 || math.Abs(distance-2.5) > 1e-12 {
|
||
t.Fatalf("RA %.4f 的赤纬/距离 = %.6f / %.6f,want -5.8 / 2.5", want, dec, distance)
|
||
}
|
||
}
|
||
if _, _, _, ok := occultationPathVectorRaDec(occultationPathVector{}); ok {
|
||
t.Fatal("零矢量应判为无效")
|
||
}
|
||
}
|