151 lines
5.1 KiB
Go
151 lines
5.1 KiB
Go
|
|
package basic
|
||
|
|
|
||
|
|
import (
|
||
|
|
"errors"
|
||
|
|
"math"
|
||
|
|
"testing"
|
||
|
|
"time"
|
||
|
|
|
||
|
|
. "b612.me/astro/tools"
|
||
|
|
)
|
||
|
|
|
||
|
|
func TestSunRiseSetDynamicResidual(t *testing.T) {
|
||
|
|
const (
|
||
|
|
longitude = 116.4074
|
||
|
|
latitude = 39.9042
|
||
|
|
timeZone = 8.0
|
||
|
|
height = 0.0
|
||
|
|
)
|
||
|
|
jd := JDECalc(2025, 6, 5)
|
||
|
|
|
||
|
|
for _, event := range []struct {
|
||
|
|
name string
|
||
|
|
get func(float64, float64, float64, float64, float64, float64) (float64, error)
|
||
|
|
}{
|
||
|
|
{name: "rise", get: GetSunRiseTime},
|
||
|
|
{name: "set", get: GetSunSetTime},
|
||
|
|
} {
|
||
|
|
eventJD, err := event.get(jd, longitude, latitude, timeZone, 1, height)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("%s: %v", event.name, err)
|
||
|
|
}
|
||
|
|
naturalTimeZone := math.Round(longitude / 15)
|
||
|
|
localJD := eventJD + naturalTimeZone/24 - timeZone/24
|
||
|
|
residual := sunRiseSetResidual(localJD, longitude, latitude, naturalTimeZone, 1, height, -1)
|
||
|
|
if math.Abs(residual) > 0.001 {
|
||
|
|
t.Fatalf("%s dynamic horizon residual = %.9f degrees", event.name, residual)
|
||
|
|
}
|
||
|
|
|
||
|
|
fixedResidual := SunHeight(localJD, longitude, latitude, naturalTimeZone) - StandardAltitudeSun(1, height, latitude)
|
||
|
|
if math.Abs(fixedResidual) < 0.01 {
|
||
|
|
t.Fatalf("%s still matches the legacy fixed-altitude event: residual %.9f degrees", event.name, fixedResidual)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestSunApparentStateReusesDistanceWithoutChangingCoordinates(t *testing.T) {
|
||
|
|
const jd = 2460827.5
|
||
|
|
ra, dec, distanceAU := hSunApparentRaDecDistanceN(jd, -1)
|
||
|
|
wantRA, wantDec := LoBoToRaDec(jd, HSunApparentLoN(jd, -1), HSunTrueBoN(jd, -1))
|
||
|
|
assertClose(t, "sun state RA", ra, wantRA, 1e-12)
|
||
|
|
assertClose(t, "sun state Dec", dec, wantDec, 1e-12)
|
||
|
|
assertClose(t, "sun state distance", distanceAU, EarthAwayN(jd, -1), 1e-15)
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestSunRiseSetDynamicGrazingKeepsDateAndDirection(t *testing.T) {
|
||
|
|
date := time.Date(2025, 6, 10, 0, 0, 0, 0, time.UTC)
|
||
|
|
jd := Date2JDE(date)
|
||
|
|
dayStart := math.Floor(jd) + 0.5
|
||
|
|
|
||
|
|
rise, err := GetSunRiseTime(jd, 0, 66, 0, 1, 0)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("sunrise: %v", err)
|
||
|
|
}
|
||
|
|
set, err := GetSunSetTime(jd, 0, 66, 0, 1, 0)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("sunset: %v", err)
|
||
|
|
}
|
||
|
|
|
||
|
|
assertRiseSetEvent(t, "sunrise", rise, dayStart, true, func(eventJD float64) float64 {
|
||
|
|
return sunRiseSetResidual(eventJD, 0, 66, 0, 1, 0, -1)
|
||
|
|
})
|
||
|
|
assertRiseSetEvent(t, "sunset", set, dayStart, false, func(eventJD float64) float64 {
|
||
|
|
return sunRiseSetResidual(eventJD, 0, 66, 0, 1, 0, -1)
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestMoonSetDynamicGrazingKeepsDirection(t *testing.T) {
|
||
|
|
date := time.Date(2025, 1, 31, 0, 0, 0, 0, time.UTC)
|
||
|
|
jd := Date2JDE(date)
|
||
|
|
dayStart := math.Floor(jd) + 0.5
|
||
|
|
set, err := GetMoonSetTime(jd, 0, 80, 0, 1, 0)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("moonset: %v", err)
|
||
|
|
}
|
||
|
|
assertRiseSetEvent(t, "moonset", set, dayStart, false, func(eventJD float64) float64 {
|
||
|
|
return moonRiseSetResidual(eventJD, 0, 80, 0, 1, 0, -1)
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestMoonRiseSetDirectionalFallbackPreservesMissingEventError(t *testing.T) {
|
||
|
|
date := time.Date(2024, 2, 29, 0, 0, 0, 0, time.UTC)
|
||
|
|
jd := Date2JDE(date)
|
||
|
|
_, err := GetMoonRiseTime(jd, -42.6043, 71.7069, -3, 1, 0)
|
||
|
|
if !errors.Is(err, ErrNeverRise) {
|
||
|
|
t.Fatalf("moonrise error = %v, want %v", err, ErrNeverRise)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestMoonRiseSetDynamicUsesObserverHeightForParallax(t *testing.T) {
|
||
|
|
date := time.Date(2025, 6, 5, 0, 0, 0, 0, time.UTC)
|
||
|
|
jd := Date2JDE(date)
|
||
|
|
const (
|
||
|
|
longitude = 116.4074
|
||
|
|
latitude = 39.9042
|
||
|
|
height = 10000.0
|
||
|
|
)
|
||
|
|
rise, err := GetMoonRiseTime(jd, longitude, latitude, 0, 1, height)
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("moonrise: %v", err)
|
||
|
|
}
|
||
|
|
residual := moonRiseSetResidualAtObserverHeight(rise, longitude, latitude, 0, 1, height)
|
||
|
|
if math.Abs(residual) > 1e-5 {
|
||
|
|
t.Fatalf("moonrise observer-height residual = %.12f degrees", residual)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func assertRiseSetEvent(t *testing.T, name string, eventJD, dayStart float64, isRise bool, residual func(float64) float64) {
|
||
|
|
t.Helper()
|
||
|
|
if eventJD < dayStart || eventJD >= dayStart+1 {
|
||
|
|
t.Fatalf("%s %.12f is outside civil day [%.12f, %.12f)", name, eventJD, dayStart, dayStart+1)
|
||
|
|
}
|
||
|
|
const step = 1.0 / 1440
|
||
|
|
slope := (residual(eventJD+step) - residual(eventJD-step)) / (2 * step)
|
||
|
|
if isRise && slope <= 0 {
|
||
|
|
t.Fatalf("%s slope = %.9f degrees/day, want positive", name, slope)
|
||
|
|
}
|
||
|
|
if !isRise && slope >= 0 {
|
||
|
|
t.Fatalf("%s slope = %.9f degrees/day, want negative", name, slope)
|
||
|
|
}
|
||
|
|
if value := residual(eventJD); math.Abs(value) > 1e-4 {
|
||
|
|
t.Fatalf("%s residual = %.12f degrees", name, value)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func moonRiseSetResidualAtObserverHeight(jd, longitude, latitude, timeZone, zenithShift, height float64) float64 {
|
||
|
|
calculationJD := TD2UT(jd-timeZone/24, true)
|
||
|
|
ra, dec := HMoonTrueRaDecN(calculationJD, -1)
|
||
|
|
distanceKM := HMoonAwayN(calculationJD, -1)
|
||
|
|
topocentricRA, topocentricDec := TopocentricRaDec(ra, dec, latitude, longitude,
|
||
|
|
jd-timeZone/24, distanceKM/angularDiameterAstronomicalUnitKM, height)
|
||
|
|
siderealTime := Limit360(ApparentSiderealTime(jd-timeZone/24)*15 + longitude)
|
||
|
|
hourAngle := Limit360(siderealTime - topocentricRA)
|
||
|
|
altitude := ArcSin(Sin(latitude)*Sin(topocentricDec) + Cos(topocentricDec)*Cos(latitude)*Cos(hourAngle))
|
||
|
|
residual := altitude + HeightDegreeByLat(height, latitude)
|
||
|
|
if zenithShift != 0 {
|
||
|
|
residual += RefractionFromTrueAltitude(altitude, refractionStandardPressureHPa, refractionStandardTemperatureC)
|
||
|
|
residual += angularSemidiameterArcsec(moonEquatorialRadiusKM, distanceKM) / 3600
|
||
|
|
}
|
||
|
|
return residual
|
||
|
|
}
|