326 lines
12 KiB
Go
326 lines
12 KiB
Go
|
|
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
|
||
|
|
}
|