- 新增日食、月食、本地可见性、中心线、半影区域、SVG 图示与沙罗周期信息 - 新增行星冲合、留、方照、物理星历、视直径、相位、亮肢角、轨道节点等计算 - 新增木星伽利略卫星位置、现象与接触事件计算 - 新增恒星星表、星座判定、自行修正与观测辅助能力 - 新增 coord、formula、orbit、sundial、lite/sun、lite/moon 等扩展包 - 完善农历年号、月相英文别名、视差角、大气质量、折射、日晷与双星计算 - 增加 NASA、JPL Horizons、IMCCE 等回归测试数据与基线测试 - 重构基础算法文件组织,补充大量公开 API 注释和语义回归测试 - 更新中文和英文 README,补充示例、精度说明、SVG 配图
309 lines
12 KiB
Go
309 lines
12 KiB
Go
package basic
|
|
|
|
import (
|
|
"math"
|
|
"testing"
|
|
)
|
|
|
|
type lunarEclipseBaseline struct {
|
|
name string
|
|
jde float64
|
|
|
|
expectedType LunarEclipseType
|
|
expectedMax float64
|
|
expectedMag float64
|
|
|
|
expectedPenumbralStart float64
|
|
expectedPenumbralEnd float64
|
|
expectedPartialStart float64
|
|
expectedPartialEnd float64
|
|
expectedTotalStart float64
|
|
expectedTotalEnd float64
|
|
}
|
|
|
|
func TestLunarEclipseChauvenetAgainstLegacyBaseline(t *testing.T) {
|
|
// 这些基准值来自历史本地月食基线,
|
|
// 其阴影口径对应当前保留的 Chauvenet 模型。
|
|
testCases := []lunarEclipseBaseline{
|
|
{
|
|
name: "2022-11-08 total",
|
|
jde: JDECalc(2022, 11, 8),
|
|
expectedType: LunarEclipseTotal,
|
|
expectedMax: 2459891.9585873615,
|
|
expectedMag: 1.3635170051692678,
|
|
expectedPenumbralStart: 2459891.8346063416,
|
|
expectedPenumbralEnd: 2459892.0826413140,
|
|
expectedPartialStart: 2459891.8820205650,
|
|
expectedPartialEnd: 2459892.0351211606,
|
|
expectedTotalStart: 2459891.9288277230,
|
|
expectedTotalEnd: 2459891.9883249460,
|
|
},
|
|
{
|
|
name: "2023-05-05 penumbral",
|
|
jde: JDECalc(2023, 5, 5),
|
|
expectedType: LunarEclipsePenumbral,
|
|
expectedPenumbralStart: 2460070.1342392800,
|
|
expectedPenumbralEnd: 2460070.3159191823,
|
|
},
|
|
{
|
|
name: "2023-10-28 partial",
|
|
jde: JDECalc(2023, 10, 28),
|
|
expectedType: LunarEclipsePartial,
|
|
expectedMax: 2460246.3439460830,
|
|
expectedMag: 0.12723850274626405,
|
|
expectedPenumbralStart: 2460246.2507697106,
|
|
expectedPenumbralEnd: 2460246.4370874465,
|
|
expectedPartialStart: 2460246.3164327650,
|
|
expectedPartialEnd: 2460246.3713359070,
|
|
},
|
|
{
|
|
name: "2024-03-25 penumbral",
|
|
jde: JDECalc(2024, 3, 25),
|
|
expectedType: LunarEclipsePenumbral,
|
|
expectedPenumbralStart: 2460394.7028870000,
|
|
expectedPenumbralEnd: 2460394.8999071894,
|
|
},
|
|
{
|
|
name: "2024-09-18 partial",
|
|
jde: JDECalc(2024, 9, 18),
|
|
expectedType: LunarEclipsePartial,
|
|
expectedMax: 2460571.6148748010,
|
|
expectedMag: 0.09042791952817894,
|
|
expectedPenumbralStart: 2460571.5281155687,
|
|
expectedPenumbralEnd: 2460571.7016473800,
|
|
expectedPartialStart: 2460571.5923644140,
|
|
expectedPartialEnd: 2460571.6374154520,
|
|
},
|
|
{
|
|
name: "2025-03-14 total",
|
|
jde: JDECalc(2025, 3, 14),
|
|
expectedType: LunarEclipseTotal,
|
|
expectedMax: 2460748.7916214615,
|
|
expectedMag: 1.1828107517800281,
|
|
expectedPenumbralStart: 2460748.6645233813,
|
|
expectedPenumbralEnd: 2460748.9187600957,
|
|
expectedPartialStart: 2460748.7156107454,
|
|
expectedPartialEnd: 2460748.8676076555,
|
|
expectedTotalStart: 2460748.7685903380,
|
|
expectedTotalEnd: 2460748.8146345600,
|
|
},
|
|
{
|
|
name: "2025-09-07 total",
|
|
jde: JDECalc(2025, 9, 7),
|
|
expectedType: LunarEclipseTotal,
|
|
expectedMax: 2460926.2590034613,
|
|
expectedMag: 1.3672329695760280,
|
|
expectedPenumbralStart: 2460926.1445036167,
|
|
expectedPenumbralEnd: 2460926.3734498024,
|
|
expectedPartialStart: 2460926.1860739910,
|
|
expectedPartialEnd: 2460926.3319619163,
|
|
expectedTotalStart: 2460926.2302397094,
|
|
expectedTotalEnd: 2460926.2877871464,
|
|
},
|
|
{
|
|
name: "2026-03-03 total",
|
|
jde: JDECalc(2026, 3, 3),
|
|
expectedType: LunarEclipseTotal,
|
|
expectedMax: 2461102.9825476190,
|
|
expectedMag: 1.1556387222746651,
|
|
expectedPenumbralStart: 2461102.8638708987,
|
|
expectedPenumbralEnd: 2461103.1012840020,
|
|
expectedPartialStart: 2461102.9103626400,
|
|
expectedPartialEnd: 2461103.0546894810,
|
|
expectedTotalStart: 2461102.9619182530,
|
|
expectedTotalEnd: 2461103.0031447060,
|
|
},
|
|
}
|
|
|
|
const timeTolerance = 1e-6
|
|
const magnitudeTolerance = 2e-5
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
result := LunarEclipseChauvenet(tc.jde)
|
|
|
|
if result.Type != tc.expectedType {
|
|
t.Fatalf("Type mismatch: got %s want %s", result.Type, tc.expectedType)
|
|
}
|
|
|
|
if tc.expectedMax != 0 && math.Abs(result.Maximum-tc.expectedMax) > timeTolerance {
|
|
t.Fatalf("Maximum mismatch: got %.12f want %.12f", result.Maximum, tc.expectedMax)
|
|
}
|
|
if tc.expectedMag != 0 && math.Abs(result.Magnitude-tc.expectedMag) > magnitudeTolerance {
|
|
t.Fatalf("Magnitude mismatch: got %.12f want %.12f", result.Magnitude, tc.expectedMag)
|
|
}
|
|
|
|
assertCloseJD(t, "PenumbralStart", result.PenumbralStart, tc.expectedPenumbralStart, timeTolerance)
|
|
assertCloseJD(t, "PenumbralEnd", result.PenumbralEnd, tc.expectedPenumbralEnd, timeTolerance)
|
|
assertCloseJD(t, "PartialStart", result.PartialStart, tc.expectedPartialStart, timeTolerance)
|
|
assertCloseJD(t, "PartialEnd", result.PartialEnd, tc.expectedPartialEnd, timeTolerance)
|
|
assertCloseJD(t, "TotalStart", result.TotalStart, tc.expectedTotalStart, timeTolerance)
|
|
assertCloseJD(t, "TotalEnd", result.TotalEnd, tc.expectedTotalEnd, timeTolerance)
|
|
|
|
if result.HasTotal && !(result.TotalStart < result.Maximum && result.Maximum < result.TotalEnd) {
|
|
t.Fatalf("total contact order invalid: start=%.12f max=%.12f end=%.12f", result.TotalStart, result.Maximum, result.TotalEnd)
|
|
}
|
|
if result.HasPartial && !(result.PartialStart < result.Maximum && result.Maximum < result.PartialEnd) {
|
|
t.Fatalf("partial contact order invalid: start=%.12f max=%.12f end=%.12f", result.PartialStart, result.Maximum, result.PartialEnd)
|
|
}
|
|
if result.HasPenumbral && !(result.PenumbralStart < result.Maximum && result.Maximum < result.PenumbralEnd) {
|
|
t.Fatalf("penumbral contact order invalid: start=%.12f max=%.12f end=%.12f", result.PenumbralStart, result.Maximum, result.PenumbralEnd)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestLunarEclipseDefaultUsesDanjon(t *testing.T) {
|
|
jde := JDECalc(2025, 3, 14)
|
|
defaultResult := LunarEclipse(jde)
|
|
danjonResult := LunarEclipseDanjon(jde)
|
|
chauvenetResult := LunarEclipseChauvenet(jde)
|
|
|
|
assertCloseJD(t, "Maximum", defaultResult.Maximum, danjonResult.Maximum, 1e-12)
|
|
assertCloseJD(t, "PenumbralStart", defaultResult.PenumbralStart, danjonResult.PenumbralStart, 1e-12)
|
|
assertCloseJD(t, "PenumbralEnd", defaultResult.PenumbralEnd, danjonResult.PenumbralEnd, 1e-12)
|
|
if math.Abs(defaultResult.PenumbralMagnitude-danjonResult.PenumbralMagnitude) > 1e-12 {
|
|
t.Fatalf("default penumbral magnitude mismatch: got %.12f want %.12f", defaultResult.PenumbralMagnitude, danjonResult.PenumbralMagnitude)
|
|
}
|
|
if math.Abs(defaultResult.PenumbralMagnitude-chauvenetResult.PenumbralMagnitude) < 1e-4 {
|
|
t.Fatalf("default model should not collapse to Chauvenet: default=%.12f chauvenet=%.12f", defaultResult.PenumbralMagnitude, chauvenetResult.PenumbralMagnitude)
|
|
}
|
|
}
|
|
|
|
func TestPenumbralLunarEclipseKeepsNegativeUmbralMagnitude(t *testing.T) {
|
|
testCases := []struct {
|
|
name string
|
|
jde float64
|
|
calc func(float64) LunarEclipseResult
|
|
}{
|
|
{name: "default 2024-03-25", jde: JDECalc(2024, 3, 25), calc: LunarEclipse},
|
|
{name: "danjon 2024-03-25", jde: JDECalc(2024, 3, 25), calc: LunarEclipseDanjon},
|
|
{name: "chauvenet 2023-05-05", jde: JDECalc(2023, 5, 5), calc: LunarEclipseChauvenet},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
result := tc.calc(tc.jde)
|
|
if result.Type != LunarEclipsePenumbral {
|
|
t.Fatalf("type mismatch: got %s want %s", result.Type, LunarEclipsePenumbral)
|
|
}
|
|
if !result.HasPenumbral || result.HasPartial || result.HasTotal {
|
|
t.Fatalf("unexpected eclipse flags: %+v", result)
|
|
}
|
|
if !(result.Magnitude < 0) {
|
|
t.Fatalf("expected negative umbral magnitude for penumbral eclipse, got %.12f", result.Magnitude)
|
|
}
|
|
if !(result.PenumbralMagnitude > 0) {
|
|
t.Fatalf("expected positive penumbral magnitude, got %.12f", result.PenumbralMagnitude)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestLunarEclipseDanjonMagnitudesCloserToNASA(t *testing.T) {
|
|
testCases := []struct {
|
|
name string
|
|
jde float64
|
|
expectedType LunarEclipseType
|
|
nasaPenumbralMagnitude float64
|
|
nasaUmbralMagnitude float64
|
|
}{
|
|
{
|
|
name: "2023-10-28 partial",
|
|
jde: JDECalc(2023, 10, 28),
|
|
expectedType: LunarEclipsePartial,
|
|
nasaPenumbralMagnitude: 1.1181,
|
|
nasaUmbralMagnitude: 0.1220,
|
|
},
|
|
{
|
|
name: "2025-03-14 total",
|
|
jde: JDECalc(2025, 3, 14),
|
|
expectedType: LunarEclipseTotal,
|
|
nasaPenumbralMagnitude: 2.2595,
|
|
nasaUmbralMagnitude: 1.1784,
|
|
},
|
|
{
|
|
name: "2026-03-03 total",
|
|
jde: JDECalc(2026, 3, 3),
|
|
expectedType: LunarEclipseTotal,
|
|
nasaPenumbralMagnitude: 2.1838,
|
|
nasaUmbralMagnitude: 1.1507,
|
|
},
|
|
{
|
|
name: "2026-08-28 partial",
|
|
jde: JDECalc(2026, 8, 28),
|
|
expectedType: LunarEclipsePartial,
|
|
nasaPenumbralMagnitude: 1.9645,
|
|
nasaUmbralMagnitude: 0.9299,
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
danjonResult := LunarEclipseDanjon(tc.jde)
|
|
chauvenetResult := LunarEclipseChauvenet(tc.jde)
|
|
|
|
if danjonResult.Type != tc.expectedType {
|
|
t.Fatalf("Danjon type mismatch: got %s want %s", danjonResult.Type, tc.expectedType)
|
|
}
|
|
if chauvenetResult.Type != tc.expectedType {
|
|
t.Fatalf("Chauvenet type mismatch: got %s want %s", chauvenetResult.Type, tc.expectedType)
|
|
}
|
|
|
|
danjonPenumbralError := math.Abs(danjonResult.PenumbralMagnitude - tc.nasaPenumbralMagnitude)
|
|
chauvenetPenumbralError := math.Abs(chauvenetResult.PenumbralMagnitude - tc.nasaPenumbralMagnitude)
|
|
if !(danjonPenumbralError < chauvenetPenumbralError) {
|
|
t.Fatalf("Danjon penumbral magnitude should be closer to NASA: danjon=%.6f chauvenet=%.6f nasa=%.6f", danjonResult.PenumbralMagnitude, chauvenetResult.PenumbralMagnitude, tc.nasaPenumbralMagnitude)
|
|
}
|
|
|
|
danjonUmbralError := math.Abs(danjonResult.Magnitude - tc.nasaUmbralMagnitude)
|
|
chauvenetUmbralError := math.Abs(chauvenetResult.Magnitude - tc.nasaUmbralMagnitude)
|
|
if !(danjonUmbralError < chauvenetUmbralError) {
|
|
t.Fatalf("Danjon umbral magnitude should be closer to NASA: danjon=%.6f chauvenet=%.6f nasa=%.6f", danjonResult.Magnitude, chauvenetResult.Magnitude, tc.nasaUmbralMagnitude)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestLunarEclipseNoEvent(t *testing.T) {
|
|
testCases := []struct {
|
|
name string
|
|
calc func(float64) LunarEclipseResult
|
|
}{
|
|
{name: "default", calc: LunarEclipse},
|
|
{name: "danjon", calc: LunarEclipseDanjon},
|
|
{name: "chauvenet", calc: LunarEclipseChauvenet},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
result := tc.calc(JDECalc(2023, 6, 4))
|
|
if result.Type != LunarEclipseNone {
|
|
t.Fatalf("Type mismatch: got %s want %s", result.Type, LunarEclipseNone)
|
|
}
|
|
if result.HasPenumbral || result.HasPartial || result.HasTotal {
|
|
t.Fatalf("unexpected contacts: %+v", result)
|
|
}
|
|
if result.PenumbralStart != 0 || result.PenumbralEnd != 0 || result.PartialStart != 0 || result.PartialEnd != 0 || result.TotalStart != 0 || result.TotalEnd != 0 {
|
|
t.Fatalf("expected no contact times, got %+v", result)
|
|
}
|
|
if result.Magnitude != 0 || result.PenumbralMagnitude != 0 {
|
|
t.Fatalf("expected zero magnitudes for non-eclipse, got %+v", result)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func assertCloseJD(t *testing.T, name string, got, want, tolerance float64) {
|
|
t.Helper()
|
|
if want == 0 {
|
|
if got != 0 {
|
|
t.Fatalf("%s mismatch: got %.12f want 0", name, got)
|
|
}
|
|
return
|
|
}
|
|
if math.Abs(got-want) > tolerance {
|
|
t.Fatalf("%s mismatch: got %.12f want %.12f", name, got, want)
|
|
}
|
|
}
|