2026-05-01 22:38:44 +08:00
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 )
}
2026-09-17 12:27:40 +08:00
assertContactJD ( t , "PenumbralStart" , result . PenumbralStart , tc . expectedPenumbralStart , timeTolerance )
assertContactJD ( t , "PenumbralEnd" , result . PenumbralEnd , tc . expectedPenumbralEnd , timeTolerance )
assertContactJD ( t , "PartialStart" , result . PartialStart , tc . expectedPartialStart , timeTolerance )
assertContactJD ( t , "PartialEnd" , result . PartialEnd , tc . expectedPartialEnd , timeTolerance )
assertContactJD ( t , "TotalStart" , result . TotalStart , tc . expectedTotalStart , timeTolerance )
assertContactJD ( t , "TotalEnd" , result . TotalEnd , tc . expectedTotalEnd , timeTolerance )
2026-05-01 22:38:44 +08:00
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 )
}
2026-09-17 12:27:40 +08:00
for name , value := range map [ string ] float64 {
"PenumbralStart" : result . PenumbralStart ,
"PenumbralEnd" : result . PenumbralEnd ,
"PartialStart" : result . PartialStart ,
"PartialEnd" : result . PartialEnd ,
"TotalStart" : result . TotalStart ,
"TotalEnd" : result . TotalEnd ,
} {
if ! math . IsNaN ( value ) {
t . Fatalf ( "%s = %.12f, want NaN for a non-eclipse: %+v" , name , value , result )
}
2026-05-01 22:38:44 +08:00
}
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 )
}
}
2026-09-17 12:27:40 +08:00
// assertContactJD 未发生阶段的期望值是 0(基准表写法),此时实际值必须是 NaN 而不是 JD 0。
func assertContactJD ( t * testing . T , name string , got , want , tolerance float64 ) {
t . Helper ()
if want == 0 {
if ! math . IsNaN ( got ) {
t . Fatalf ( "%s = %.12f, want NaN for an absent phase" , name , got )
}
return
}
assertCloseJD ( t , name , got , want , tolerance )
}