Files
astro/basic/solar_eclipse_bessel_test.go
T

253 lines
9.5 KiB
Go
Raw Normal View History

package basic
import (
"math"
"testing"
)
// 根数表口径:经典贝塞尔符号、NASA 式三次拟合,以及与已发布表的 μ 时间自变量换算。
const (
besselianNASAT0JDE = 2460409.25
besselianNASADeltaT = 70.6
besselianNASASeed = 2460409.262835
)
func nasa2024BesselianElements(t *testing.T) SolarEclipseBesselianElementsResult {
t.Helper()
result, ok := SolarEclipseBesselianElements(besselianNASASeed, SolarEclipseBesselianElementsOptions{
DeltaTSeconds: besselianNASADeltaT,
ReferenceJDE: besselianNASAT0JDE,
})
if !ok {
t.Fatal("2024-04-08 should have a solar eclipse")
}
return result
}
func TestSolarEclipseBesselianElementsMatchesNASA2024(t *testing.T) {
result := nasa2024BesselianElements(t)
cases := []struct {
label string
got SolarEclipseBesselianPolynomial
want SolarEclipseBesselianPolynomial
tol [4]float64
}{
{"x", result.X, SolarEclipseBesselianPolynomial{-0.318157, 0.5117105, 0.0000326, -0.0000085}, [4]float64{2e-4, 1e-5, 1e-6, 1e-6}},
{"y", result.Y, SolarEclipseBesselianPolynomial{0.219747, 0.2709586, -0.0000594, -0.0000047}, [4]float64{2e-4, 1e-5, 1e-6, 1e-6}},
{"d", result.D, SolarEclipseBesselianPolynomial{7.58620, 0.014844, -0.000002, 0}, [4]float64{1e-4, 1e-5, 1e-5, 1e-5}},
{"l1", result.L1, SolarEclipseBesselianPolynomial{0.535813, 0.0000618, -0.0000128, 0}, [4]float64{1e-4, 1e-5, 1e-5, 1e-5}},
{"l2", result.L2, SolarEclipseBesselianPolynomial{-0.010274, 0.0000615, -0.0000127, 0}, [4]float64{1e-4, 1e-5, 1e-5, 1e-5}},
}
for _, item := range cases {
for n := range item.want {
if math.Abs(item.got[n]-item.want[n]) > item.tol[n] {
t.Errorf("%s[%d] = %.8f, want %.8f (tol %.1e)", item.label, n, item.got[n], item.want[n], item.tol[n])
}
}
}
published := SolarEclipseBesselianMuForPublishedTable(result.Mu, result.DeltaTSeconds)
for n, want := range [4]float64{89.59122, 15.004084, 0, 0} {
if math.Abs(published[n]-want) > 1e-4 {
t.Errorf("published mu[%d] = %.6f, want %.6f", n, published[n], want)
}
}
if math.Abs(result.TanF1-0.0046683) > 1e-6 || math.Abs(result.TanF2-0.0046450) > 1e-6 {
t.Errorf("tan f = %.8f / %.8f, want 0.0046683 / 0.0046450", result.TanF1, result.TanF2)
}
if math.Abs(result.Gamma-0.3431) > 1e-4 {
t.Errorf("gamma = %.6f, want 0.3431", result.Gamma)
}
if math.Abs(result.Magnitude-1.0566) > 1e-3 {
t.Errorf("magnitude = %.6f, want 1.0566", result.Magnitude)
}
if result.T0JDE != besselianNASAT0JDE || result.ValidHours != 3 {
t.Errorf("t0 = %.6f validHours = %g, want %.6f / 3", result.T0JDE, result.ValidHours, besselianNASAT0JDE)
}
if result.Model != SolarEclipseModelNASABulletinSplitK {
t.Errorf("model = %q, want %q", result.Model, SolarEclipseModelNASABulletinSplitK)
}
if result.PenumbralK != solarEclipsePenumbralK || result.UmbralK != solarEclipseUmbralK {
t.Errorf("k = %g / %g, want %g / %g", result.PenumbralK, result.UmbralK, solarEclipsePenumbralK, solarEclipseUmbralK)
}
if math.Abs(result.DeltaTSeconds-besselianNASADeltaT) > 1e-9 {
t.Errorf("deltaT = %g, want %g", result.DeltaTSeconds, besselianNASADeltaT)
}
}
func TestSolarEclipseBesselianElementsFollowsSampledElements(t *testing.T) {
result := nasa2024BesselianElements(t)
solver := newSolarEclipseSolver(
CalcMoonSHByJDE(besselianNASASeed, 0),
SolarEclipseModelNASABulletinSplitK,
).withDeltaTSeconds(besselianNASADeltaT)
for _, hours := range []float64{-2.7, -0.8, 0.4, 2.3} {
point := solver.besselianElementsAt(result.T0JDE + hours/24)
if math.Abs(result.X.At(hours)-point.x) > 1e-5 ||
math.Abs(result.Y.At(hours)-point.y) > 1e-5 ||
math.Abs(result.L1.At(hours)-point.l1) > 1e-5 ||
math.Abs(result.L2.At(hours)-point.l2) > 1e-5 ||
math.Abs(result.D.At(hours)-point.d) > 1e-5 {
t.Errorf("t=%+.2fh polynomial (%g,%g,%g,%g,%g) does not track elements (%g,%g,%g,%g,%g)",
hours, result.X.At(hours), result.Y.At(hours), result.D.At(hours), result.L1.At(hours), result.L2.At(hours),
point.x, point.y, point.d, point.l1, point.l2)
}
if math.Abs(math.Mod(result.Mu.At(hours)-point.mu, 360)) > 1e-4 {
t.Errorf("t=%+.2fh mu = %g does not track %g", hours, result.Mu.At(hours), point.mu)
}
}
}
func TestSolarEclipseBesselianElementsMuStaysContinuousAcrossZero(t *testing.T) {
// 2013-11-03 的食甚点在格林尼治附近,μ 的采样点会跨过 0°,是展开逻辑的边界用例。
for _, seed := range []float64{besselianNASASeed, JDCalc(2013, 11, 3)} {
result, ok := SolarEclipseBesselianElements(seed, SolarEclipseBesselianElementsOptions{
DeltaTSeconds: besselianNASADeltaT,
})
if !ok {
t.Fatalf("seed %.4f should have a solar eclipse", seed)
}
if result.Mu.At(0) < 0 || result.Mu.At(0) >= 360 {
t.Errorf("seed %.4f: mu(0) = %g, want [0,360)", seed, result.Mu.At(0))
}
if math.Abs(result.Mu[1]-15.041067) > 0.05 {
t.Errorf("seed %.4f: mu rate = %.6f, want about 15.041067", seed, result.Mu[1])
}
for _, hours := range []float64{-3, -1.5, 1.5, 3} {
drift := result.Mu.At(hours) - (result.Mu.At(0) + 15.041067*hours)
if math.Abs(drift) > 0.5 {
t.Errorf("seed %.4f: mu(%+.1f) is not a continuous continuation, drift %.4f", seed, hours, drift)
}
}
}
}
func TestSolarEclipseBesselianElementsDefaultsToWholeHour(t *testing.T) {
result, ok := SolarEclipseBesselianElements(besselianNASASeed, SolarEclipseBesselianElementsOptions{
DeltaTSeconds: besselianNASADeltaT,
})
if !ok {
t.Fatal("2024-04-08 should have a solar eclipse")
}
if math.Abs(result.T0JDE*24-math.Round(result.T0JDE*24)) > 1e-9 {
t.Errorf("t0 = %.9f is not a whole TT hour", result.T0JDE)
}
if math.Abs(result.T0JDE*24-besselianNASASeed*24) > 0.5 {
t.Errorf("t0 = %.6f is not the whole hour nearest greatest eclipse %.6f", result.T0JDE, besselianNASASeed)
}
if result.ValidHours != 3 {
t.Errorf("validHours = %g, want 3", result.ValidHours)
}
// 食甚落在半小时之后的事件取下一个整小时:NASA 2009-07-22 表食甚 02:36:24 TDT,t0 记 03:00 TDT。
forward, ok := SolarEclipseBesselianElements(JDCalc(2009, 7, 22), SolarEclipseBesselianElementsOptions{
DeltaTSeconds: 65.9,
})
if !ok {
t.Fatal("2009-07-22 should have a solar eclipse")
}
if math.Abs(forward.T0JDE-2455034.625) > 1e-9 {
t.Errorf("t0 = %.9f, want 2455034.625 (03:00 TDT)", forward.T0JDE)
}
}
func TestSolarEclipseBesselianElementsMatchesPublishedHours(t *testing.T) {
testCases := []struct {
name string
date [3]int
delta float64
t0 float64
want [6]SolarEclipseBesselianPolynomial
}{
{
name: "2009-07-22 total", date: [3]int{2009, 7, 22}, delta: 65.9, t0: 2455034.625,
want: [6]SolarEclipseBesselianPolynomial{
{0.240059, 0.5563975, -0.0000583, -0.0000100},
{-0.003283, -0.1774571, -0.0001346, 0.0000032},
{20.26424, -0.007873, -0.000004, 0},
{0.530426, 0.0000063, -0.0000128, 0},
{-0.015633, 0.0000063, -0.0000127, 0},
{0, 0, 0, 0},
},
},
{
name: "2013-11-03 hybrid", date: [3]int{2013, 11, 3}, delta: 67.2, t0: 2456600.041666667,
want: [6]SolarEclipseBesselianPolynomial{
{0.183190, 0.5469478, 0.0000282, -0.0000083},
{0.294721, -0.1200753, 0.0000790, 0.0000017},
{-15.20965, -0.012636, 0.000003, 0},
{0.546301, -0.0001121, -0.0000120, 0},
{0.000143, -0.0001116, -0.0000120, 0},
{0, 0, 0, 0},
},
},
}
tolerances := [6][4]float64{
{2e-4, 1e-5, 1e-6, 1e-6},
{2e-4, 1e-5, 1e-6, 1e-6},
{1e-4, 1e-5, 1e-5, 1e-5},
{1e-4, 1e-5, 1e-5, 1e-5},
{1e-4, 1e-5, 1e-5, 1e-5},
{1e-4, 1e-5, 1e-5, 1e-5},
}
labels := [6]string{"x", "y", "d", "l1", "l2", "mu"}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
result, ok := SolarEclipseBesselianElements(JDCalc(tc.date[0], tc.date[1], float64(tc.date[2])),
SolarEclipseBesselianElementsOptions{DeltaTSeconds: tc.delta})
if !ok {
t.Fatalf("%s should have a solar eclipse", tc.name)
}
if math.Abs(result.T0JDE-tc.t0) > 1e-9 {
t.Fatalf("t0 = %.9f, want %.9f", result.T0JDE, tc.t0)
}
got := [6]SolarEclipseBesselianPolynomial{result.X, result.Y, result.D, result.L1, result.L2, result.Mu}
for index := range got {
if index == 5 {
continue
}
for n := range tc.want[index] {
if math.Abs(got[index][n]-tc.want[index][n]) > tolerances[index][n] {
t.Errorf("%s[%d] = %.8f, want %.8f (tol %.1e)", labels[index], n,
got[index][n], tc.want[index][n], tolerances[index][n])
}
}
}
})
}
}
func TestSolarEclipseBesselianElementsIAUSingleKUsesOneK(t *testing.T) {
result, ok := SolarEclipseBesselianElements(besselianNASASeed, SolarEclipseBesselianElementsOptions{
Model: SolarEclipseModelIAUSingleK,
DeltaTSeconds: besselianNASADeltaT,
})
if !ok {
t.Fatal("2024-04-08 should have a solar eclipse")
}
if result.Model != SolarEclipseModelIAUSingleK {
t.Errorf("model = %q, want %q", result.Model, SolarEclipseModelIAUSingleK)
}
if result.UmbralK != result.PenumbralK {
t.Errorf("k2 = %g, want k1 = %g", result.UmbralK, result.PenumbralK)
}
}
func TestSolarEclipseBesselianElementsWithoutEclipse(t *testing.T) {
if _, ok := SolarEclipseBesselianElements(JDCalc(2024, 5, 8), SolarEclipseBesselianElementsOptions{}); ok {
t.Fatal("2024-05-08 has no solar eclipse")
}
}
func TestSolarEclipseBesselianElementsRejectsShortSampleSet(t *testing.T) {
if got := solarEclipseFitCubic([]float64{-1, 0, 1}, []float64{1, 2, 3}); got != (SolarEclipseBesselianPolynomial{}) {
t.Errorf("fit with three samples = %v, want zero", got)
}
if got := solarEclipseFitCubic([]float64{-3, -1.5, 0, 1.5, 3}, []float64{0, 0, 0, 0, 0}); got != (SolarEclipseBesselianPolynomial{}) {
t.Errorf("fit of a zero column = %v, want zero", got)
}
}