Files
astro/basic/moon_rise_set_external_test.go
T

326 lines
12 KiB
Go
Raw Normal View History

package basic
import (
"encoding/json"
"fmt"
"math"
"os"
"testing"
"time"
"b612.me/astro/tools"
)
type moonRiseSetExternalEvents struct {
RiseUTC string `json:"rise_utc"`
SetUTC string `json:"set_utc"`
}
type moonRiseSetExternalSample struct {
Site string `json:"site"`
DateUTC string `json:"date_utc"`
Longitude float64 `json:"longitude"`
Latitude float64 `json:"latitude"`
ObserverHeight float64 `json:"observer_height_m"`
Horizons moonRiseSetExternalEvents `json:"jpl_horizons"`
METNorway moonRiseSetExternalEvents `json:"met_norway"`
IMCCEMiriade moonRiseSetExternalEvents `json:"imcce_miriade"`
}
type moonRiseSetExternalBaseline struct {
SchemaVersion int `json:"schema_version"`
Sources map[string]struct {
Provider string `json:"provider"`
Model string `json:"model"`
} `json:"sources"`
Samples []moonRiseSetExternalSample `json:"samples"`
}
type moonRiseSetErrorStats struct {
Total time.Duration
Max time.Duration
Count int
}
func (stats *moonRiseSetErrorStats) Add(value time.Duration) {
stats.Total += value
stats.Count++
if value > stats.Max {
stats.Max = value
}
}
func (stats moonRiseSetErrorStats) Mean() time.Duration {
if stats.Count == 0 {
return 0
}
return stats.Total / time.Duration(stats.Count)
}
type moonRiseSetExternalTolerances struct {
Horizons time.Duration
METNorway time.Duration
IMCCEMiriade time.Duration
HorizonsVsMET time.Duration
}
type moonRiseSetComparisonStats struct {
CurrentHorizons moonRiseSetErrorStats
LegacyHorizons moonRiseSetErrorStats
CurrentMET moonRiseSetErrorStats
LegacyMET moonRiseSetErrorStats
CurrentIMCCE moonRiseSetErrorStats
LegacyIMCCE moonRiseSetErrorStats
HorizonsVsMET moonRiseSetErrorStats
HorizonsVsIMCCE moonRiseSetErrorStats
CurrentCloserJPL int
LegacyCloserJPL int
TiesJPL int
CurrentCloserMET int
LegacyCloserMET int
TiesMET int
CurrentCloserIMCCE int
LegacyCloserIMCCE int
TiesIMCCE int
}
func TestMoonRiseSetMatchesExternalBaselines(t *testing.T) {
previousDeltaT := defDeltaTFn
SetDeltaTFn(DefaultDeltaTv2)
defer SetDeltaTFn(previousDeltaT)
baseline := loadMoonRiseSetExternalBaseline(t)
if baseline.SchemaVersion != 1 {
t.Fatalf("unsupported baseline schema version %d", baseline.SchemaVersion)
}
if baseline.Sources["jpl_horizons"].Model != "DE441" {
t.Fatalf("unexpected Horizons model %q", baseline.Sources["jpl_horizons"].Model)
}
if len(baseline.Samples) < 7 {
t.Fatalf("external baseline has only %d samples", len(baseline.Samples))
}
tolerances := moonRiseSetExternalTolerances{
Horizons: 2 * time.Second,
METNorway: 90 * time.Second,
IMCCEMiriade: 8 * time.Minute,
HorizonsVsMET: 90 * time.Second,
}
var stats moonRiseSetComparisonStats
for _, sample := range baseline.Samples {
day, err := time.Parse("2006-01-02", sample.DateUTC)
if err != nil {
t.Fatalf("parse %s date %q: %v", sample.Site, sample.DateUTC, err)
}
jd := Date2JDE(day)
currentRiseJD, err := GetMoonRiseTime(jd, sample.Longitude, sample.Latitude, 0, 1, sample.ObserverHeight)
if err != nil {
t.Fatalf("%s current moonrise: %v", sample.Site, err)
}
currentSetJD, err := GetMoonSetTime(jd, sample.Longitude, sample.Latitude, 0, 1, sample.ObserverHeight)
if err != nil {
t.Fatalf("%s current moonset: %v", sample.Site, err)
}
legacyRiseJD, err := legacyMoonRiseSetFromCurrent(currentRiseJD, sample.Longitude, sample.Latitude, 0, 1, sample.ObserverHeight)
if err != nil {
t.Fatalf("%s legacy moonrise: %v", sample.Site, err)
}
legacySetJD, err := legacyMoonRiseSetFromCurrent(currentSetJD, sample.Longitude, sample.Latitude, 0, 1, sample.ObserverHeight)
if err != nil {
t.Fatalf("%s legacy moonset: %v", sample.Site, err)
}
compareMoonRiseSetEvent(t, sample.Site+".rise", currentRiseJD, legacyRiseJD,
sample.Horizons.RiseUTC, sample.METNorway.RiseUTC, sample.IMCCEMiriade.RiseUTC,
tolerances, &stats)
compareMoonRiseSetEvent(t, sample.Site+".set", currentSetJD, legacySetJD,
sample.Horizons.SetUTC, sample.METNorway.SetUTC, sample.IMCCEMiriade.SetUTC,
tolerances, &stats)
}
t.Logf("moon rise/set external baseline: current vs JPL mean=%v max=%v; legacy vs JPL mean=%v max=%v",
stats.CurrentHorizons.Mean(), stats.CurrentHorizons.Max, stats.LegacyHorizons.Mean(), stats.LegacyHorizons.Max)
t.Logf("moon rise/set external baseline: current vs MET mean=%v max=%v; legacy vs MET mean=%v max=%v",
stats.CurrentMET.Mean(), stats.CurrentMET.Max, stats.LegacyMET.Mean(), stats.LegacyMET.Max)
t.Logf("moon rise/set external baseline: current vs IMCCE mean=%v max=%v; legacy vs IMCCE mean=%v max=%v",
stats.CurrentIMCCE.Mean(), stats.CurrentIMCCE.Max, stats.LegacyIMCCE.Mean(), stats.LegacyIMCCE.Max)
t.Logf("moon rise/set external baseline: JPL vs MET mean=%v max=%v; JPL vs IMCCE mean=%v max=%v",
stats.HorizonsVsMET.Mean(), stats.HorizonsVsMET.Max, stats.HorizonsVsIMCCE.Mean(), stats.HorizonsVsIMCCE.Max)
t.Logf("moon rise/set external baseline: JPL current closer=%d legacy closer=%d ties=%d",
stats.CurrentCloserJPL, stats.LegacyCloserJPL, stats.TiesJPL)
t.Logf("moon rise/set external baseline: MET current closer=%d legacy closer=%d ties=%d",
stats.CurrentCloserMET, stats.LegacyCloserMET, stats.TiesMET)
t.Logf("moon rise/set external baseline: IMCCE current closer=%d legacy closer=%d ties=%d",
stats.CurrentCloserIMCCE, stats.LegacyCloserIMCCE, stats.TiesIMCCE)
}
func TestMoonRiseSetLegacyComparatorMatchesPreFixSnapshot(t *testing.T) {
previousDeltaT := defDeltaTFn
SetDeltaTFn(DefaultDeltaTv2)
defer SetDeltaTFn(previousDeltaT)
jd := JDECalc(2023, 1, 15)
currentRise, err := GetMoonRiseTime(jd, 116.4074, 39.9042, 8, 1, 0)
if err != nil {
t.Fatalf("current moonrise: %v", err)
}
currentSet, err := GetMoonSetTime(jd, 116.4074, 39.9042, 8, 1, 0)
if err != nil {
t.Fatalf("current moonset: %v", err)
}
legacyRise, err := legacyMoonRiseSetFromCurrent(currentRise, 116.4074, 39.9042, 8, 1, 0)
if err != nil {
t.Fatalf("legacy moonrise: %v", err)
}
legacySet, err := legacyMoonRiseSetFromCurrent(currentSet, 116.4074, 39.9042, 8, 1, 0)
if err != nil {
t.Fatalf("legacy moonset: %v", err)
}
const snapshotTolerance = 2.0 / 86400
if difference := math.Abs(legacyRise - 2459959.509182); difference > snapshotTolerance {
t.Errorf("legacy moonrise snapshot mismatch: got %.9f want %.9f difference=%.3fs",
legacyRise, 2459959.509182, difference*86400)
}
if difference := math.Abs(legacySet - 2459959.988676); difference > snapshotTolerance {
t.Errorf("legacy moonset snapshot mismatch: got %.9f want %.9f difference=%.3fs",
legacySet, 2459959.988676, difference*86400)
}
}
func loadMoonRiseSetExternalBaseline(t *testing.T) moonRiseSetExternalBaseline {
t.Helper()
data, err := os.ReadFile("testdata/moon_rise_set_baseline.json")
if err != nil {
t.Fatalf("read moon rise/set baseline: %v", err)
}
var baseline moonRiseSetExternalBaseline
if err := json.Unmarshal(data, &baseline); err != nil {
t.Fatalf("decode moon rise/set baseline: %v", err)
}
return baseline
}
func compareMoonRiseSetEvent(t *testing.T, name string, currentJD, legacyJD float64,
horizonsUTC, metUTC, imcceUTC string, tolerances moonRiseSetExternalTolerances,
stats *moonRiseSetComparisonStats) {
t.Helper()
current := JDE2DateByZone(currentJD, time.UTC, false)
legacy := JDE2DateByZone(legacyJD, time.UTC, false)
horizons := parseMoonRiseSetExternalTime(t, name+".jpl", horizonsUTC)
met := parseMoonRiseSetExternalTime(t, name+".met", metUTC)
imcce := parseMoonRiseSetExternalTime(t, name+".imcce", imcceUTC)
currentHorizonsError := absoluteTimeDifference(current, horizons)
legacyHorizonsError := absoluteTimeDifference(legacy, horizons)
currentMETError := absoluteTimeDifference(current, met)
legacyMETError := absoluteTimeDifference(legacy, met)
currentIMCCEError := absoluteTimeDifference(current, imcce)
legacyIMCCEError := absoluteTimeDifference(legacy, imcce)
stats.CurrentHorizons.Add(currentHorizonsError)
stats.LegacyHorizons.Add(legacyHorizonsError)
stats.CurrentMET.Add(currentMETError)
stats.LegacyMET.Add(legacyMETError)
stats.CurrentIMCCE.Add(currentIMCCEError)
stats.LegacyIMCCE.Add(legacyIMCCEError)
horizonsVsMET := absoluteTimeDifference(horizons, met)
stats.HorizonsVsMET.Add(horizonsVsMET)
stats.HorizonsVsIMCCE.Add(absoluteTimeDifference(horizons, imcce))
if currentHorizonsError > tolerances.Horizons {
t.Errorf("%s current mismatch against JPL: got %s want %s difference=%v tolerance=%v",
name, current.Format(time.RFC3339Nano), horizonsUTC, currentHorizonsError, tolerances.Horizons)
}
if currentMETError > tolerances.METNorway {
t.Errorf("%s current mismatch against MET Norway: got %s want %s difference=%v tolerance=%v",
name, current.Format(time.RFC3339Nano), metUTC, currentMETError, tolerances.METNorway)
}
if currentIMCCEError > tolerances.IMCCEMiriade {
t.Errorf("%s current mismatch against IMCCE Miriade: got %s want %s difference=%v tolerance=%v",
name, current.Format(time.RFC3339Nano), imcceUTC, currentIMCCEError, tolerances.IMCCEMiriade)
}
if horizonsVsMET > tolerances.HorizonsVsMET {
t.Errorf("%s external sources disagree: JPL=%s MET=%s difference=%v tolerance=%v",
name, horizonsUTC, metUTC, horizonsVsMET, tolerances.HorizonsVsMET)
}
switch {
case currentHorizonsError < legacyHorizonsError:
stats.CurrentCloserJPL++
case legacyHorizonsError < currentHorizonsError:
stats.LegacyCloserJPL++
default:
stats.TiesJPL++
}
switch {
case currentMETError < legacyMETError:
stats.CurrentCloserMET++
case legacyMETError < currentMETError:
stats.LegacyCloserMET++
default:
stats.TiesMET++
}
switch {
case currentIMCCEError < legacyIMCCEError:
stats.CurrentCloserIMCCE++
case legacyIMCCEError < currentIMCCEError:
stats.LegacyCloserIMCCE++
default:
stats.TiesIMCCE++
}
t.Logf("%s current_jpl=%v legacy_jpl=%v current_met=%v legacy_met=%v current_imcce=%v legacy_imcce=%v", name,
currentHorizonsError, legacyHorizonsError, currentMETError, legacyMETError, currentIMCCEError, legacyIMCCEError)
}
func parseMoonRiseSetExternalTime(t *testing.T, name, value string) time.Time {
t.Helper()
parsed, err := time.Parse(time.RFC3339, value)
if err != nil {
t.Fatalf("parse %s time %q: %v", name, value, err)
}
return parsed
}
func absoluteTimeDifference(left, right time.Time) time.Duration {
difference := left.Sub(right)
if difference < 0 {
return -difference
}
return difference
}
func legacyMoonRiseSetFromCurrent(currentJD, longitude, latitude, timeZone, zenithShift, height float64) (float64, error) {
localTimeZone := longitude / 15
localJD := currentJD + localTimeZone/24 - timeZone/24
targetAltitude := StandardAltitudeMoon(zenithShift, height, latitude)
legacyJD := moonRiseSetNewtonRaphsonIteration(localJD, longitude, latitude, localTimeZone,
targetAltitude, legacyHMoonHeight, 0.00002)
if math.IsNaN(legacyJD) || math.IsInf(legacyJD, 0) {
return 0, fmt.Errorf("legacy height iteration did not converge")
}
return legacyJD - localTimeZone/24 + timeZone/24, nil
}
func legacyHMoonHeight(jd, longitude, latitude, timeZone float64) float64 {
calculationJD := TD2UT(jd-timeZone/24, true)
ra, dec := HMoonTrueRaDecN(calculationJD, -1)
distanceAU := HMoonAwayN(calculationJD, -1) / 149597870.7
topocentricRA, topocentricDec := legacyTopocentricRaDec(ra, dec, latitude, longitude, calculationJD, distanceAU, 0)
siderealTime := tools.Limit360(ApparentSiderealTime(jd-timeZone/24)*15 + longitude)
hourAngle := tools.Limit360(siderealTime - topocentricRA)
altitudeSine := tools.Sin(latitude)*tools.Sin(topocentricDec) +
tools.Cos(topocentricDec)*tools.Cos(latitude)*tools.Cos(hourAngle)
return tools.ArcSin(altitudeSine)
}
func legacyTopocentricRaDec(ra, dec, latitude, longitude, jd, distanceAU, height float64) (float64, float64) {
horizontalParallaxSine := tools.Sin(0.0024427777777) / distanceAU
observerCosine := pcosi(latitude, height)
observerSine := psini(latitude, height)
hourAngle := tools.Limit360(TD2UT(ApparentSiderealTime(jd), false)*15 + longitude - ra)
raCorrection := math.Atan2(-observerCosine*horizontalParallaxSine*tools.Sin(hourAngle),
tools.Cos(dec)-observerCosine*horizontalParallaxSine*tools.Cos(hourAngle)) * 180 / math.Pi
correctedDec := math.Atan2((tools.Sin(dec)-observerSine*horizontalParallaxSine)*tools.Cos(raCorrection),
tools.Cos(dec)-observerCosine*horizontalParallaxSine*tools.Cos(hourAngle)) * 180 / math.Pi
return ra + raCorrection, correctedDec
}