astro/basic/solar_eclipse_test.go

194 lines
6.8 KiB
Go
Raw Normal View History

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