astro/basic/lunar_eclipse_test.go

309 lines
12 KiB
Go
Raw Normal View History

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)
}
}