197 lines
7.8 KiB
Go
197 lines
7.8 KiB
Go
|
|
package basic
|
||
|
|
|
||
|
|
import (
|
||
|
|
"math"
|
||
|
|
"testing"
|
||
|
|
"time"
|
||
|
|
)
|
||
|
|
|
||
|
|
func TestSolarEclipseSarosFamilyRemainsFiniteAcrossFiveCenturies(t *testing.T) {
|
||
|
|
base := JDECalc(2024, 4, 8)
|
||
|
|
const sarosDays = 6585.321314
|
||
|
|
for familyIndex := -28; familyIndex <= 28; familyIndex++ {
|
||
|
|
seed := base + float64(familyIndex)*sarosDays
|
||
|
|
result := SolarEclipse(seed)
|
||
|
|
for name, value := range map[string]float64{
|
||
|
|
"greatest": result.GreatestEclipse,
|
||
|
|
"gamma": result.Gamma,
|
||
|
|
"magnitude": result.Magnitude,
|
||
|
|
"longitude": result.GreatestLongitude,
|
||
|
|
"latitude": result.GreatestLatitude,
|
||
|
|
} {
|
||
|
|
if !finite(value) {
|
||
|
|
t.Fatalf("saros family index %d %s=%v", familyIndex, name, value)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if result.HasPartial && !(result.PartialBeginOnEarth <= result.GreatestEclipse &&
|
||
|
|
result.GreatestEclipse <= result.PartialEndOnEarth) {
|
||
|
|
t.Fatalf("saros family index %d partial window does not contain greatest: %+v", familyIndex, result)
|
||
|
|
}
|
||
|
|
if result.HasCentral && !(result.CentralBeginOnEarth <= result.GreatestEclipse &&
|
||
|
|
result.GreatestEclipse <= result.CentralEndOnEarth) {
|
||
|
|
t.Fatalf("saros family index %d central window does not contain greatest: %+v", familyIndex, result)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestSolarEclipseRepresentativePathSeriesAreOrderedAndFinite(t *testing.T) {
|
||
|
|
cases := []struct {
|
||
|
|
name string
|
||
|
|
seed float64
|
||
|
|
}{
|
||
|
|
{name: "2009-07-22", seed: JDECalc(2009, 7, 22)},
|
||
|
|
{name: "2010-01-15", seed: JDECalc(2010, 1, 15)},
|
||
|
|
{name: "2014-04-29-non-central", seed: JDECalc(2014, 4, 29)},
|
||
|
|
{name: "2023-04-20", seed: JDECalc(2023, 4, 20)},
|
||
|
|
{name: "2043-10-03", seed: JDECalc(2043, 10, 3)},
|
||
|
|
}
|
||
|
|
for _, test := range cases {
|
||
|
|
t.Run(test.name, func(t *testing.T) {
|
||
|
|
result := SolarEclipsePartialFootprints(test.seed, SolarEclipsePartialFootprintOptions{
|
||
|
|
StepDays: 20.0 / 1440.0, BoundaryPoints: 24,
|
||
|
|
CentralShadowStepDays: 20.0 / 1440.0,
|
||
|
|
DisableRiseSetCurves: true,
|
||
|
|
})
|
||
|
|
if !result.Eclipse.HasPartial {
|
||
|
|
t.Fatalf("expected partial eclipse, got %+v", result.Eclipse)
|
||
|
|
}
|
||
|
|
if test.name == "2014-04-29-non-central" &&
|
||
|
|
(result.Eclipse.Centrality != SolarEclipseNonCentral || len(result.CentralBandSegments) == 0) {
|
||
|
|
t.Fatalf("non-central eclipse lost centrality envelope: centrality=%s segments=%d",
|
||
|
|
result.Eclipse.Centrality, len(result.CentralBandSegments))
|
||
|
|
}
|
||
|
|
assertSolarEclipseFootprintSeriesFinite(t, result.Footprints)
|
||
|
|
assertSolarEclipseFootprintSeriesFinite(t, result.CentralShadowFootprints)
|
||
|
|
assertSolarEclipseFootprintSeriesFinite(t, result.CentralBandFootprints)
|
||
|
|
for index := 1; index < len(result.CentralBandSegments); index++ {
|
||
|
|
if len(result.CentralBandSegments[index]) == 0 {
|
||
|
|
t.Fatalf("central band segment %d is empty", index)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
})
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func assertSolarEclipseFootprintSeriesFinite(t *testing.T, footprints []SolarEclipsePartialFootprint) {
|
||
|
|
t.Helper()
|
||
|
|
for index, footprint := range footprints {
|
||
|
|
if !finite(footprint.JDE) {
|
||
|
|
t.Fatalf("footprint %d has invalid JDE=%v", index, footprint.JDE)
|
||
|
|
}
|
||
|
|
for boundaryIndex, boundary := range footprint.Boundaries {
|
||
|
|
if len(boundary) < 2 {
|
||
|
|
t.Fatalf("footprint %d boundary %d has %d points", index, boundaryIndex, len(boundary))
|
||
|
|
}
|
||
|
|
for pointIndex, point := range boundary {
|
||
|
|
if !finite(point.JDE) || !finite(point.Longitude) || !finite(point.Latitude) || !finite(point.SunAltitude) {
|
||
|
|
t.Fatalf("footprint %d boundary %d point %d is invalid: %+v", index, boundaryIndex, pointIndex, point)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if footprint.Closed {
|
||
|
|
totalPoints := 0
|
||
|
|
for _, boundary := range footprint.Boundaries {
|
||
|
|
totalPoints += len(boundary)
|
||
|
|
}
|
||
|
|
if totalPoints < 3 {
|
||
|
|
t.Fatalf("closed footprint %d has only %d points", index, totalPoints)
|
||
|
|
}
|
||
|
|
if len(footprint.Boundaries) == 1 {
|
||
|
|
boundary := footprint.Boundaries[0]
|
||
|
|
if solarEclipsePathDistanceKM(boundary[0], boundary[len(boundary)-1]) > 5 {
|
||
|
|
t.Fatalf("closed footprint %d has %.3f km endpoint gap", index,
|
||
|
|
solarEclipsePathDistanceKM(boundary[0], boundary[len(boundary)-1]))
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestOccultationFiniteDiskStatesRemainValidAcrossFiveCenturies(t *testing.T) {
|
||
|
|
base := occultationTimeToTT(time.Date(2025, time.January, 5, 0, 0, 0, 0, time.UTC))
|
||
|
|
for _, yearOffset := range []float64{-500, -250, 0, 250, 500} {
|
||
|
|
tt := base + yearOffset*365.2425
|
||
|
|
for _, planet := range []OccultationPlanet{
|
||
|
|
OccultationMercury, OccultationVenus, OccultationMars,
|
||
|
|
OccultationJupiter, OccultationSaturn, OccultationUranus, OccultationNeptune,
|
||
|
|
} {
|
||
|
|
config, ok := planetOccultationConfigFor(planet)
|
||
|
|
if !ok {
|
||
|
|
t.Fatalf("%s configuration unavailable", planet)
|
||
|
|
}
|
||
|
|
state := planetOccultationStateAt(tt, config, nil, -1)
|
||
|
|
if !state.valid {
|
||
|
|
t.Fatalf("year offset %.0f %s state is invalid: %+v", yearOffset, planet, state)
|
||
|
|
}
|
||
|
|
if !finite(state.externalContactMetric) || !finite(state.internalContactMetric) {
|
||
|
|
t.Fatalf("year offset %.0f %s contact metrics are not finite: %+v", yearOffset, planet, state)
|
||
|
|
}
|
||
|
|
if state.internalContactMetric < state.externalContactMetric {
|
||
|
|
t.Fatalf("year offset %.0f %s inner gap %.9f is below outer gap %.9f",
|
||
|
|
yearOffset, planet, state.internalContactMetric, state.externalContactMetric)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
star := starOccultationEphemerisStateAt(tt, hr4799OccultationCoordinateForTest())
|
||
|
|
if !star.valid || !finite(star.moonDistanceKM) || !finite(star.starRA) || !finite(star.starDec) {
|
||
|
|
t.Fatalf("year offset %.0f star state is invalid: %+v", yearOffset, star)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestOccultationRepresentativePathSeriesAreOrderedAndFinite(t *testing.T) {
|
||
|
|
zone := time.FixedZone("UTC+8", 8*60*60)
|
||
|
|
planetPaths, err := FindPlanetOccultationPaths(
|
||
|
|
time.Date(2025, time.January, 5, 0, 0, 0, 0, zone),
|
||
|
|
time.Date(2025, time.January, 6, 0, 0, 0, 0, zone),
|
||
|
|
OccultationSaturn,
|
||
|
|
OccultationPathOptions{Step: 20 * time.Minute, TargetSpacingKM: 900, DisableFootprints: true, DisableRiseSet: true},
|
||
|
|
)
|
||
|
|
if err != nil || len(planetPaths) != 1 {
|
||
|
|
t.Fatalf("planet paths=%d err=%v, want one", len(planetPaths), err)
|
||
|
|
}
|
||
|
|
assertOccultationPointSeriesFinite(t, planetPaths[0].CenterLine)
|
||
|
|
assertOccultationPointSeriesFinite(t, planetPaths[0].NorthernLimit)
|
||
|
|
assertOccultationPointSeriesFinite(t, planetPaths[0].SouthernLimit)
|
||
|
|
if planetPaths[0].HasTotalBand {
|
||
|
|
assertOccultationPointSeriesFinite(t, planetPaths[0].NorthernTotalLimit)
|
||
|
|
assertOccultationPointSeriesFinite(t, planetPaths[0].SouthernTotalLimit)
|
||
|
|
}
|
||
|
|
|
||
|
|
starPaths, err := FindStarOccultationPaths(
|
||
|
|
time.Date(2025, time.June, 5, 0, 0, 0, 0, zone),
|
||
|
|
time.Date(2025, time.June, 6, 0, 0, 0, 0, zone),
|
||
|
|
hr4799OccultationCoordinateForTest(),
|
||
|
|
OccultationPathOptions{Step: 20 * time.Minute, TargetSpacingKM: 900, DisableFootprints: true, DisableRiseSet: true},
|
||
|
|
)
|
||
|
|
if err != nil || len(starPaths) != 1 {
|
||
|
|
t.Fatalf("star paths=%d err=%v, want one", len(starPaths), err)
|
||
|
|
}
|
||
|
|
assertOccultationPointSeriesFinite(t, starPaths[0].CenterLine)
|
||
|
|
assertOccultationPointSeriesFinite(t, starPaths[0].NorthernLimit)
|
||
|
|
assertOccultationPointSeriesFinite(t, starPaths[0].SouthernLimit)
|
||
|
|
}
|
||
|
|
|
||
|
|
func assertOccultationPointSeriesFinite(t *testing.T, points []OccultationPathPoint) {
|
||
|
|
t.Helper()
|
||
|
|
if len(points) == 0 {
|
||
|
|
t.Fatal("path series is empty")
|
||
|
|
}
|
||
|
|
for index, point := range points {
|
||
|
|
if !finite(point.Longitude) || !finite(point.Latitude) || !finite(point.WidthKM) {
|
||
|
|
t.Fatalf("point %d is not finite: %+v", index, point)
|
||
|
|
}
|
||
|
|
if index > 0 && !point.Time.After(points[index-1].Time) {
|
||
|
|
t.Fatalf("path times are not strictly increasing at %d: %v then %v", index, points[index-1].Time, point.Time)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestSolarEclipseRepresentativePathPointsDoNotContainNaN(t *testing.T) {
|
||
|
|
path := SolarEclipseCentralPath(JDECalc(2010, 1, 15), SolarEclipsePathOptions{StepDays: 20.0 / 1440.0})
|
||
|
|
for index, point := range append(append(append([]SolarEclipsePathPoint{}, path.CenterLine...), path.NorthernLimit...), path.SouthernLimit...) {
|
||
|
|
if math.IsNaN(point.Longitude) || math.IsNaN(point.Latitude) || math.IsNaN(point.JDE) {
|
||
|
|
t.Fatalf("path point %d contains NaN: %+v", index, point)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|