261 lines
10 KiB
Go
261 lines
10 KiB
Go
|
|
package basic
|
||
|
|
|
||
|
|
import (
|
||
|
|
"encoding/json"
|
||
|
|
"math"
|
||
|
|
"os"
|
||
|
|
"testing"
|
||
|
|
"time"
|
||
|
|
)
|
||
|
|
|
||
|
|
type moonPlanetConjunctionBaselineSample struct {
|
||
|
|
Planet string `json:"planet"`
|
||
|
|
Year int `json:"year"`
|
||
|
|
Month int `json:"month"`
|
||
|
|
TimeUTC string `json:"time_utc"`
|
||
|
|
}
|
||
|
|
|
||
|
|
type moonPlanetConjunctionBaseline struct {
|
||
|
|
Samples []moonPlanetConjunctionBaselineSample `json:"samples"`
|
||
|
|
}
|
||
|
|
|
||
|
|
func loadMoonPlanetConjunctionBaseline(t *testing.T) moonPlanetConjunctionBaseline {
|
||
|
|
t.Helper()
|
||
|
|
|
||
|
|
paths := [][]string{
|
||
|
|
{
|
||
|
|
"testdata/moon_planet_conjunction_baseline.json",
|
||
|
|
"basic/testdata/moon_planet_conjunction_baseline.json",
|
||
|
|
},
|
||
|
|
{
|
||
|
|
"testdata/moon_planet_conjunction_baseline_samples.json",
|
||
|
|
"basic/testdata/moon_planet_conjunction_baseline_samples.json",
|
||
|
|
},
|
||
|
|
}
|
||
|
|
|
||
|
|
var merged moonPlanetConjunctionBaseline
|
||
|
|
for index, candidates := range paths {
|
||
|
|
var (
|
||
|
|
data []byte
|
||
|
|
err error
|
||
|
|
)
|
||
|
|
for _, path := range candidates {
|
||
|
|
data, err = os.ReadFile(path)
|
||
|
|
if err == nil {
|
||
|
|
var baseline moonPlanetConjunctionBaseline
|
||
|
|
if err := json.Unmarshal(data, &baseline); err != nil {
|
||
|
|
t.Fatalf("decode baseline %s: %v", path, err)
|
||
|
|
}
|
||
|
|
merged.Samples = append(merged.Samples, baseline.Samples...)
|
||
|
|
break
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if err != nil && index == 0 {
|
||
|
|
t.Fatalf("read baseline: %v", err)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if len(merged.Samples) == 0 {
|
||
|
|
t.Fatal("empty moon-planet conjunction baseline")
|
||
|
|
}
|
||
|
|
return merged
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestMoonPlanetConjunctionsMatchHorizonsBaseline(t *testing.T) {
|
||
|
|
baseline := loadMoonPlanetConjunctionBaseline(t)
|
||
|
|
|
||
|
|
type conjunctionCase struct {
|
||
|
|
planet MoonPlanetConjunctionPlanet
|
||
|
|
next func(float64, MoonPlanetConjunctionPlanet) float64
|
||
|
|
}
|
||
|
|
|
||
|
|
cases := map[string]conjunctionCase{
|
||
|
|
"mercury": {planet: MoonPlanetConjunctionMercury, next: NextMoonPlanetConjunction},
|
||
|
|
"venus": {planet: MoonPlanetConjunctionVenus, next: NextMoonPlanetConjunction},
|
||
|
|
"mars": {planet: MoonPlanetConjunctionMars, next: NextMoonPlanetConjunction},
|
||
|
|
"jupiter": {planet: MoonPlanetConjunctionJupiter, next: NextMoonPlanetConjunction},
|
||
|
|
"saturn": {planet: MoonPlanetConjunctionSaturn, next: NextMoonPlanetConjunction},
|
||
|
|
"uranus": {planet: MoonPlanetConjunctionUranus, next: NextMoonPlanetConjunction},
|
||
|
|
"neptune": {planet: MoonPlanetConjunctionNeptune, next: NextMoonPlanetConjunction},
|
||
|
|
}
|
||
|
|
|
||
|
|
const tolerance = 20 * time.Second
|
||
|
|
var maxDiff time.Duration
|
||
|
|
|
||
|
|
seen := make(map[string]int, len(cases))
|
||
|
|
for _, sample := range baseline.Samples {
|
||
|
|
tc, ok := cases[sample.Planet]
|
||
|
|
if !ok {
|
||
|
|
t.Fatalf("unknown planet %q", sample.Planet)
|
||
|
|
}
|
||
|
|
|
||
|
|
wantTime, err := time.Parse(time.RFC3339Nano, sample.TimeUTC)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("parse sample time %q: %v", sample.TimeUTC, err)
|
||
|
|
}
|
||
|
|
queryTT := TD2UT(Date2JDE(wantTime.Add(-12*time.Hour).UTC()), true)
|
||
|
|
gotUT := tc.next(queryTT, tc.planet)
|
||
|
|
gotTime := JDE2DateByZone(gotUT, time.UTC, false)
|
||
|
|
diff := gotTime.Sub(wantTime)
|
||
|
|
if diff < 0 {
|
||
|
|
diff = -diff
|
||
|
|
}
|
||
|
|
if diff > maxDiff {
|
||
|
|
maxDiff = diff
|
||
|
|
}
|
||
|
|
if diff > tolerance {
|
||
|
|
t.Fatalf("%s %04d-%02d time mismatch: got %s want %s tolerance %v", sample.Planet, sample.Year, sample.Month, gotTime.Format(time.RFC3339Nano), sample.TimeUTC, tolerance)
|
||
|
|
}
|
||
|
|
|
||
|
|
delta := math.Abs(moonPlanetConjunctionDeltaAt(TD2UT(gotUT, true), tc.planet, -1))
|
||
|
|
if delta > 0.01 {
|
||
|
|
t.Fatalf("%s %04d-%02d event not near conjunction: delta=%.8f deg", sample.Planet, sample.Year, sample.Month, delta)
|
||
|
|
}
|
||
|
|
seen[sample.Planet]++
|
||
|
|
}
|
||
|
|
|
||
|
|
for planet := range cases {
|
||
|
|
if seen[planet] == 0 {
|
||
|
|
t.Fatalf("missing baseline samples for %s", planet)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
t.Logf("moon-planet conjunction max diff: time=%v", maxDiff)
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestMoonPlanetConjunctionDirectionalConsistencyAroundBaseline(t *testing.T) {
|
||
|
|
baseline := loadMoonPlanetConjunctionBaseline(t)
|
||
|
|
|
||
|
|
planets := map[string]MoonPlanetConjunctionPlanet{
|
||
|
|
"mercury": MoonPlanetConjunctionMercury,
|
||
|
|
"venus": MoonPlanetConjunctionVenus,
|
||
|
|
"mars": MoonPlanetConjunctionMars,
|
||
|
|
"jupiter": MoonPlanetConjunctionJupiter,
|
||
|
|
"saturn": MoonPlanetConjunctionSaturn,
|
||
|
|
"uranus": MoonPlanetConjunctionUranus,
|
||
|
|
"neptune": MoonPlanetConjunctionNeptune,
|
||
|
|
}
|
||
|
|
|
||
|
|
for _, sample := range baseline.Samples {
|
||
|
|
planet, ok := planets[sample.Planet]
|
||
|
|
if !ok {
|
||
|
|
t.Fatalf("unknown planet %q", sample.Planet)
|
||
|
|
}
|
||
|
|
wantTime, err := time.Parse(time.RFC3339Nano, sample.TimeUTC)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("parse sample time %q: %v", sample.TimeUTC, err)
|
||
|
|
}
|
||
|
|
queryAtTT := TD2UT(Date2JDE(wantTime.UTC()), true)
|
||
|
|
queryAfterTT := TD2UT(Date2JDE(wantTime.Add(time.Hour).UTC()), true)
|
||
|
|
|
||
|
|
exactNext := NextMoonPlanetConjunction(queryAtTT, planet)
|
||
|
|
exactClosest := ClosestMoonPlanetConjunction(queryAtTT, planet)
|
||
|
|
exactLastAfter := LastMoonPlanetConjunction(queryAfterTT, planet)
|
||
|
|
|
||
|
|
wantUT := Date2JDE(wantTime.UTC())
|
||
|
|
for name, gotUT := range map[string]float64{
|
||
|
|
"exactNext": exactNext,
|
||
|
|
"exactClosest": exactClosest,
|
||
|
|
"lastAfterEvent": exactLastAfter,
|
||
|
|
} {
|
||
|
|
gotTime := JDE2DateByZone(gotUT, time.UTC, false)
|
||
|
|
if diff := math.Abs(gotUT - wantUT); diff > 5.0/86400.0 {
|
||
|
|
t.Fatalf("%s %s mismatch: got %s want %s diff=%v", sample.Planet, name, gotTime.Format(time.RFC3339Nano), sample.TimeUTC, diff*86400)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestMoonPlanetConjunctionRejectsOppositionBranchJump(t *testing.T) {
|
||
|
|
query := time.Date(1900, 11, 10, 12, 0, 0, 0, time.UTC)
|
||
|
|
queryTT := TD2UT(Date2JDE(query), true)
|
||
|
|
|
||
|
|
lastUT := LastMoonPlanetConjunction(queryTT, MoonPlanetConjunctionSaturn)
|
||
|
|
nextUT := NextMoonPlanetConjunction(queryTT, MoonPlanetConjunctionSaturn)
|
||
|
|
|
||
|
|
if math.Abs(lastUT-Date2JDE(query)) <= 5.0/86400.0 {
|
||
|
|
t.Fatalf("last returned query time on branch jump: got %s", JDE2DateByZone(lastUT, time.UTC, false).Format(time.RFC3339Nano))
|
||
|
|
}
|
||
|
|
if math.Abs(nextUT-Date2JDE(query)) <= 5.0/86400.0 {
|
||
|
|
t.Fatalf("next returned query time on branch jump: got %s", JDE2DateByZone(nextUT, time.UTC, false).Format(time.RFC3339Nano))
|
||
|
|
}
|
||
|
|
|
||
|
|
for name, gotUT := range map[string]float64{
|
||
|
|
"last": lastUT,
|
||
|
|
"next": nextUT,
|
||
|
|
} {
|
||
|
|
delta := math.Abs(moonPlanetConjunctionDeltaAt(TD2UT(gotUT, true), MoonPlanetConjunctionSaturn, -1))
|
||
|
|
if delta > moonPlanetConjunctionEventTolerance {
|
||
|
|
t.Fatalf("%s returned non-event candidate: delta=%.8f event=%s", name, delta, JDE2DateByZone(gotUT, time.UTC, false).Format(time.RFC3339Nano))
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestMoonPlanetConjunctionDirectionalOrderingOnSampleQueries(t *testing.T) {
|
||
|
|
samples := []struct {
|
||
|
|
planet MoonPlanetConjunctionPlanet
|
||
|
|
query time.Time
|
||
|
|
}{
|
||
|
|
{planet: MoonPlanetConjunctionSaturn, query: time.Date(1700, 4, 15, 12, 0, 0, 0, time.UTC)},
|
||
|
|
{planet: MoonPlanetConjunctionMercury, query: time.Date(1900, 1, 14, 12, 0, 0, 0, time.UTC)},
|
||
|
|
{planet: MoonPlanetConjunctionVenus, query: time.Date(1950, 6, 3, 12, 0, 0, 0, time.UTC)},
|
||
|
|
{planet: MoonPlanetConjunctionMars, query: time.Date(2000, 2, 29, 18, 0, 0, 0, time.UTC)},
|
||
|
|
{planet: MoonPlanetConjunctionJupiter, query: time.Date(2026, 5, 20, 0, 0, 0, 0, time.UTC)},
|
||
|
|
{planet: MoonPlanetConjunctionSaturn, query: time.Date(2100, 8, 17, 6, 0, 0, 0, time.UTC)},
|
||
|
|
{planet: MoonPlanetConjunctionUranus, query: time.Date(2200, 11, 2, 9, 0, 0, 0, time.UTC)},
|
||
|
|
{planet: MoonPlanetConjunctionNeptune, query: time.Date(2300, 4, 24, 3, 0, 0, 0, time.UTC)},
|
||
|
|
}
|
||
|
|
|
||
|
|
for _, sample := range samples {
|
||
|
|
queryTT := TD2UT(Date2JDE(sample.query.UTC()), true)
|
||
|
|
lastUT := LastMoonPlanetConjunction(queryTT, sample.planet)
|
||
|
|
nextUT := NextMoonPlanetConjunction(queryTT, sample.planet)
|
||
|
|
closestUT := ClosestMoonPlanetConjunction(queryTT, sample.planet)
|
||
|
|
|
||
|
|
if math.IsNaN(lastUT) || math.IsNaN(nextUT) || math.IsNaN(closestUT) {
|
||
|
|
t.Fatalf("planet=%v query=%s returned NaN event(s): last=%v next=%v closest=%v", sample.planet, sample.query.Format(time.RFC3339), lastUT, nextUT, closestUT)
|
||
|
|
}
|
||
|
|
if !eventUTQueryBeforeOrEqual(lastUT, queryTT) {
|
||
|
|
t.Fatalf("planet=%v last after query: last=%s query=%s", sample.planet, JDE2DateByZone(lastUT, time.UTC, false).Format(time.RFC3339Nano), sample.query.Format(time.RFC3339Nano))
|
||
|
|
}
|
||
|
|
if !eventUTQueryAfterOrEqual(nextUT, queryTT) {
|
||
|
|
t.Fatalf("planet=%v next before query: next=%s query=%s", sample.planet, JDE2DateByZone(nextUT, time.UTC, false).Format(time.RFC3339Nano), sample.query.Format(time.RFC3339Nano))
|
||
|
|
}
|
||
|
|
if closestUT != closestEventUTToQueryTT(queryTT, lastUT, nextUT) {
|
||
|
|
t.Fatalf("planet=%v closest mismatch: got=%s want=%s", sample.planet, JDE2DateByZone(closestUT, time.UTC, false).Format(time.RFC3339Nano), JDE2DateByZone(closestEventUTToQueryTT(queryTT, lastUT, nextUT), time.UTC, false).Format(time.RFC3339Nano))
|
||
|
|
}
|
||
|
|
for name, gotUT := range map[string]float64{
|
||
|
|
"last": lastUT,
|
||
|
|
"next": nextUT,
|
||
|
|
"closest": closestUT,
|
||
|
|
} {
|
||
|
|
delta := math.Abs(moonPlanetConjunctionDeltaAt(TD2UT(gotUT, true), sample.planet, -1))
|
||
|
|
if delta > moonPlanetConjunctionEventTolerance {
|
||
|
|
t.Fatalf("planet=%v %s returned non-event candidate: delta=%.8f event=%s", sample.planet, name, delta, JDE2DateByZone(gotUT, time.UTC, false).Format(time.RFC3339Nano))
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestMoonPlanetConjunctionKeepsImmediateNeighborEvents(t *testing.T) {
|
||
|
|
query := time.Date(1700, 4, 15, 12, 0, 0, 0, time.UTC)
|
||
|
|
queryTT := TD2UT(Date2JDE(query.UTC()), true)
|
||
|
|
|
||
|
|
lastUT := LastMoonPlanetConjunction(queryTT, MoonPlanetConjunctionSaturn)
|
||
|
|
nextUT := NextMoonPlanetConjunction(queryTT, MoonPlanetConjunctionSaturn)
|
||
|
|
closestUT := ClosestMoonPlanetConjunction(queryTT, MoonPlanetConjunctionSaturn)
|
||
|
|
|
||
|
|
wantLast := time.Date(1700, 4, 15, 11, 55, 59, 115569293, time.UTC)
|
||
|
|
wantNext := time.Date(1700, 5, 13, 0, 35, 5, 981616675, time.UTC)
|
||
|
|
const tolerance = 5.0 / 86400.0
|
||
|
|
|
||
|
|
if diff := math.Abs(lastUT - Date2JDE(wantLast)); diff > tolerance {
|
||
|
|
t.Fatalf("last mismatch: got=%s want=%s diff=%.3fs", JDE2DateByZone(lastUT, time.UTC, false).Format(time.RFC3339Nano), wantLast.Format(time.RFC3339Nano), diff*86400)
|
||
|
|
}
|
||
|
|
if diff := math.Abs(nextUT - Date2JDE(wantNext)); diff > tolerance {
|
||
|
|
t.Fatalf("next mismatch: got=%s want=%s diff=%.3fs", JDE2DateByZone(nextUT, time.UTC, false).Format(time.RFC3339Nano), wantNext.Format(time.RFC3339Nano), diff*86400)
|
||
|
|
}
|
||
|
|
if !sameEventJD(closestUT, lastUT) {
|
||
|
|
t.Fatalf("closest should keep immediate previous event: closest=%s last=%s", JDE2DateByZone(closestUT, time.UTC, false).Format(time.RFC3339Nano), JDE2DateByZone(lastUT, time.UTC, false).Format(time.RFC3339Nano))
|
||
|
|
}
|
||
|
|
}
|