- 新增日食、月食、本地可见性、中心线、半影区域、SVG 图示与沙罗周期信息 - 新增行星冲合、留、方照、物理星历、视直径、相位、亮肢角、轨道节点等计算 - 新增木星伽利略卫星位置、现象与接触事件计算 - 新增恒星星表、星座判定、自行修正与观测辅助能力 - 新增 coord、formula、orbit、sundial、lite/sun、lite/moon 等扩展包 - 完善农历年号、月相英文别名、视差角、大气质量、折射、日晷与双星计算 - 增加 NASA、JPL Horizons、IMCCE 等回归测试数据与基线测试 - 重构基础算法文件组织,补充大量公开 API 注释和语义回归测试 - 更新中文和英文 README,补充示例、精度说明、SVG 配图
194 lines
6.8 KiB
Go
194 lines
6.8 KiB
Go
package basic
|
|
|
|
import (
|
|
"math"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
type solarEclipseBaseline struct {
|
|
name string
|
|
jde float64
|
|
|
|
expectedType SolarEclipseType
|
|
expectedCentrality SolarEclipseCentrality
|
|
expectedGreatestTT float64
|
|
expectedGamma float64
|
|
expectedMagnitude float64
|
|
expectedLongitude float64
|
|
expectedLatitude float64
|
|
expectedPathWidth float64
|
|
}
|
|
|
|
func TestSolarEclipseAgainstNASABaseline(t *testing.T) {
|
|
// NASA GSFC Solar Eclipse Search Engine, Besselian Elements pages:
|
|
// - 2023 Apr 20 hybrid
|
|
// - 2024 Apr 08 total
|
|
// - 2024 Oct 02 annular
|
|
// - 2025 Mar 29 partial
|
|
testCases := []solarEclipseBaseline{
|
|
{
|
|
name: "2023-04-20 hybrid",
|
|
jde: JDECalc(2023, 4, 20),
|
|
expectedType: SolarEclipseHybrid,
|
|
expectedCentrality: SolarEclipseCentralTwoLimits,
|
|
expectedGreatestTT: solarEclipseTTJDE(2023, time.April, 20, 4, 17, 56),
|
|
expectedGamma: -0.3952,
|
|
expectedMagnitude: 1.0132,
|
|
expectedLongitude: 125.8,
|
|
expectedLatitude: -9.6,
|
|
expectedPathWidth: 49.0,
|
|
},
|
|
{
|
|
name: "2024-04-08 total",
|
|
jde: JDECalc(2024, 4, 8),
|
|
expectedType: SolarEclipseTotal,
|
|
expectedCentrality: SolarEclipseCentralTwoLimits,
|
|
expectedGreatestTT: solarEclipseTTJDE(2024, time.April, 8, 18, 18, 29),
|
|
expectedGamma: 0.3431,
|
|
expectedMagnitude: 1.0566,
|
|
expectedLongitude: -104.1,
|
|
expectedLatitude: 25.3,
|
|
expectedPathWidth: 197.5,
|
|
},
|
|
{
|
|
name: "2024-10-02 annular",
|
|
jde: JDECalc(2024, 10, 2),
|
|
expectedType: SolarEclipseAnnular,
|
|
expectedCentrality: SolarEclipseCentralTwoLimits,
|
|
expectedGreatestTT: solarEclipseTTJDE(2024, time.October, 2, 18, 46, 13),
|
|
expectedGamma: -0.3509,
|
|
expectedMagnitude: 0.9326,
|
|
expectedLongitude: -114.5,
|
|
expectedLatitude: -22.0,
|
|
expectedPathWidth: 266.5,
|
|
},
|
|
{
|
|
name: "2025-03-29 partial",
|
|
jde: JDECalc(2025, 3, 29),
|
|
expectedType: SolarEclipsePartial,
|
|
expectedCentrality: SolarEclipseNonCentral,
|
|
expectedGreatestTT: solarEclipseTTJDE(2025, time.March, 29, 10, 48, 36),
|
|
expectedGamma: 1.0405,
|
|
expectedMagnitude: 0.9376,
|
|
expectedLongitude: -77.1,
|
|
expectedLatitude: 61.1,
|
|
expectedPathWidth: 0,
|
|
},
|
|
}
|
|
|
|
const (
|
|
timeToleranceDays = 2.0 / 86400.0
|
|
gammaTolerance = 5e-4
|
|
magnitudeTolerance = 5e-4
|
|
coordinateTolerance = 0.1
|
|
pathWidthTolerance = 5.0
|
|
)
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
result := SolarEclipse(tc.jde)
|
|
|
|
if result.Type != tc.expectedType {
|
|
t.Fatalf("Type mismatch: got %s want %s", result.Type, tc.expectedType)
|
|
}
|
|
if result.Centrality != tc.expectedCentrality {
|
|
t.Fatalf("Centrality mismatch: got %s want %s", result.Centrality, tc.expectedCentrality)
|
|
}
|
|
|
|
assertSolarEclipseJDEClose(t, "GreatestEclipse", result.GreatestEclipse, tc.expectedGreatestTT, timeToleranceDays)
|
|
assertSolarEclipseFloatClose(t, "Gamma", result.Gamma, tc.expectedGamma, gammaTolerance)
|
|
assertSolarEclipseFloatClose(t, "Magnitude", result.Magnitude, tc.expectedMagnitude, magnitudeTolerance)
|
|
assertSolarEclipseFloatClose(t, "GreatestLongitude", result.GreatestLongitude, tc.expectedLongitude, coordinateTolerance)
|
|
assertSolarEclipseFloatClose(t, "GreatestLatitude", result.GreatestLatitude, tc.expectedLatitude, coordinateTolerance)
|
|
assertSolarEclipseFloatClose(t, "PathWidthKM", result.PathWidthKM, tc.expectedPathWidth, pathWidthTolerance)
|
|
|
|
if result.HasPartial && !(result.PartialBeginOnEarth < result.GreatestEclipse && result.GreatestEclipse < result.PartialEndOnEarth) {
|
|
t.Fatalf(
|
|
"partial contact order invalid: begin=%.12f greatest=%.12f end=%.12f",
|
|
result.PartialBeginOnEarth, result.GreatestEclipse, result.PartialEndOnEarth,
|
|
)
|
|
}
|
|
if result.HasCentral && !(result.CentralBeginOnEarth < result.GreatestEclipse && result.GreatestEclipse < result.CentralEndOnEarth) {
|
|
t.Fatalf(
|
|
"central contact order invalid: begin=%.12f greatest=%.12f end=%.12f",
|
|
result.CentralBeginOnEarth, result.GreatestEclipse, result.CentralEndOnEarth,
|
|
)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestSolarEclipseDefaultUsesNASABulletinSplitK(t *testing.T) {
|
|
jde := JDECalc(2024, 4, 8)
|
|
defaultResult := SolarEclipse(jde)
|
|
nasaResult := SolarEclipseNASABulletinSplitK(jde)
|
|
iauResult := SolarEclipseIAUSingleK(jde)
|
|
|
|
if defaultResult.Model != SolarEclipseModelNASABulletinSplitK {
|
|
t.Fatalf("default model mismatch: got %s want %s", defaultResult.Model, SolarEclipseModelNASABulletinSplitK)
|
|
}
|
|
|
|
assertSolarEclipseJDEClose(t, "GreatestEclipse", defaultResult.GreatestEclipse, nasaResult.GreatestEclipse, 1e-12)
|
|
assertSolarEclipseFloatClose(t, "Gamma", defaultResult.Gamma, nasaResult.Gamma, 1e-12)
|
|
assertSolarEclipseFloatClose(t, "Magnitude", defaultResult.Magnitude, nasaResult.Magnitude, 1e-12)
|
|
assertSolarEclipseFloatClose(t, "PathWidthKM", defaultResult.PathWidthKM, nasaResult.PathWidthKM, 1e-12)
|
|
|
|
if math.Abs(defaultResult.PathWidthKM-iauResult.PathWidthKM) < 0.5 {
|
|
t.Fatalf(
|
|
"default model should not collapse to IAU Single-K: default=%.6f iau=%.6f",
|
|
defaultResult.PathWidthKM, iauResult.PathWidthKM,
|
|
)
|
|
}
|
|
if !(iauResult.PathWidthKM > defaultResult.PathWidthKM) {
|
|
t.Fatalf(
|
|
"IAU Single-K should produce a wider total path than NASA Split-K here: iau=%.6f default=%.6f",
|
|
iauResult.PathWidthKM, defaultResult.PathWidthKM,
|
|
)
|
|
}
|
|
}
|
|
|
|
func TestSolarEclipseNoEvent(t *testing.T) {
|
|
testCases := []struct {
|
|
name string
|
|
calc func(float64) SolarEclipseResult
|
|
}{
|
|
{name: "default", calc: SolarEclipse},
|
|
{name: "nasa", calc: SolarEclipseNASABulletinSplitK},
|
|
{name: "iau", calc: SolarEclipseIAUSingleK},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
result := tc.calc(JDECalc(2023, 5, 15))
|
|
if result.Type != SolarEclipseNone {
|
|
t.Fatalf("Type mismatch: got %s want %s", result.Type, SolarEclipseNone)
|
|
}
|
|
if result.HasPartial || result.HasCentral || result.HasAnnular || result.HasTotal || result.HasHybrid {
|
|
t.Fatalf("unexpected eclipse flags: %+v", result)
|
|
}
|
|
if result.PartialBeginOnEarth != 0 || result.PartialEndOnEarth != 0 || result.CentralBeginOnEarth != 0 || result.CentralEndOnEarth != 0 {
|
|
t.Fatalf("expected no contact times, got %+v", result)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func solarEclipseTTJDE(year int, month time.Month, day, hour, minute, second int) float64 {
|
|
return Date2JDE(time.Date(year, month, day, hour, minute, second, 0, time.UTC))
|
|
}
|
|
|
|
func assertSolarEclipseJDEClose(t *testing.T, name string, got, want, tolerance float64) {
|
|
t.Helper()
|
|
if math.Abs(got-want) > tolerance {
|
|
t.Fatalf("%s mismatch: got %.12f want %.12f", name, got, want)
|
|
}
|
|
}
|
|
|
|
func assertSolarEclipseFloatClose(t *testing.T, name string, got, want, tolerance float64) {
|
|
t.Helper()
|
|
if math.Abs(got-want) > tolerance {
|
|
t.Fatalf("%s mismatch: got %.9f want %.9f", name, got, want)
|
|
}
|
|
}
|