199 lines
8.0 KiB
Go
199 lines
8.0 KiB
Go
|
|
package moon
|
||
|
|
|
||
|
|
import (
|
||
|
|
"errors"
|
||
|
|
"math"
|
||
|
|
"testing"
|
||
|
|
"time"
|
||
|
|
|
||
|
|
"b612.me/astro/basic"
|
||
|
|
)
|
||
|
|
|
||
|
|
func TestFindStarOccultationsFindsPointSourceEvent(t *testing.T) {
|
||
|
|
observer := Observer{Longitude: 121.4737, Latitude: 31.2304, Height: 4}
|
||
|
|
eventTime := time.Date(2026, 8, 2, 12, 0, 0, 0, time.UTC)
|
||
|
|
tt := occultationTestTimeToTT(eventTime)
|
||
|
|
ra, dec := occultationTestMoonTopocentricRaDec(tt, observer, -1)
|
||
|
|
star := StarCoordinate{
|
||
|
|
ID: "synthetic-point-source",
|
||
|
|
RA: ra,
|
||
|
|
Dec: dec,
|
||
|
|
Epoch: eventTime,
|
||
|
|
Frame: CoordinateFrameApparentOfDate,
|
||
|
|
}
|
||
|
|
|
||
|
|
start := eventTime.In(time.FixedZone("CST", 8*3600)).Add(-3 * time.Hour)
|
||
|
|
end := eventTime.In(time.FixedZone("CST", 8*3600)).Add(3 * time.Hour)
|
||
|
|
results, err := FindStarOccultations(start, end, star,
|
||
|
|
observer.Longitude, observer.Latitude, observer.Height, OccultationSearchOptions{MaxEvents: 1})
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("FindStarOccultations() error = %v", err)
|
||
|
|
}
|
||
|
|
if len(results) != 1 {
|
||
|
|
t.Fatalf("FindStarOccultations() returned %d events, want 1", len(results))
|
||
|
|
}
|
||
|
|
result := results[0]
|
||
|
|
if result.Observer != observer {
|
||
|
|
t.Fatalf("observer = %+v, want %+v", result.Observer, observer)
|
||
|
|
}
|
||
|
|
if result.Greatest.Location().String() != "CST" {
|
||
|
|
t.Fatalf("greatest location = %q, want CST", result.Greatest.Location().String())
|
||
|
|
}
|
||
|
|
if result.Type != OccultationTotal {
|
||
|
|
t.Fatalf("event type = %q, want %q", result.Type, OccultationTotal)
|
||
|
|
}
|
||
|
|
if result.MinimumSeparationArcsec > result.MoonSemidiameterArcsec {
|
||
|
|
t.Fatalf("minimum separation %.3f exceeds lunar radius %.3f", result.MinimumSeparationArcsec, result.MoonSemidiameterArcsec)
|
||
|
|
}
|
||
|
|
if result.Immersion.IsZero() || result.Emersion.IsZero() {
|
||
|
|
t.Fatalf("contact times must be populated: immersion=%v emersion=%v", result.Immersion, result.Emersion)
|
||
|
|
}
|
||
|
|
if !result.ContactsComplete {
|
||
|
|
t.Fatal("contacts must be marked complete")
|
||
|
|
}
|
||
|
|
if !(result.Immersion.Before(result.Greatest) && result.Greatest.Before(result.Emersion)) {
|
||
|
|
t.Fatalf("contact ordering invalid: immersion=%v greatest=%v emersion=%v", result.Immersion, result.Greatest, result.Emersion)
|
||
|
|
}
|
||
|
|
if delta := math.Abs(result.Greatest.Sub(eventTime).Seconds()); delta > 60 {
|
||
|
|
t.Fatalf("greatest differs from synthetic event by %.1fs", delta)
|
||
|
|
}
|
||
|
|
if result.MoonAzimuthAtGreatest < 0 || result.MoonAzimuthAtGreatest >= 360 {
|
||
|
|
t.Fatalf("moon azimuth = %.6f, want [0,360)", result.MoonAzimuthAtGreatest)
|
||
|
|
}
|
||
|
|
if result.PositionAngleDeg < 0 || result.PositionAngleDeg >= 360 {
|
||
|
|
t.Fatalf("position angle = %.6f, want [0,360)", result.PositionAngleDeg)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestFindBestStarOccultationsReturnsTopocentricGroundPoint(t *testing.T) {
|
||
|
|
observer := Observer{Longitude: 121.4737, Latitude: 31.2304, Height: 4}
|
||
|
|
eventTime := time.Date(2026, 8, 2, 12, 0, 0, 0, time.UTC)
|
||
|
|
ra, dec := occultationTestMoonTopocentricRaDec(occultationTestTimeToTT(eventTime), observer, -1)
|
||
|
|
star := StarCoordinate{
|
||
|
|
ID: "synthetic-best-point-source",
|
||
|
|
RA: ra,
|
||
|
|
Dec: dec,
|
||
|
|
Epoch: eventTime,
|
||
|
|
Frame: CoordinateFrameApparentOfDate,
|
||
|
|
}
|
||
|
|
|
||
|
|
results, err := FindBestStarOccultations(
|
||
|
|
eventTime.Add(-3*time.Hour), eventTime.Add(3*time.Hour), star, OccultationSearchOptions{MaxEvents: 1})
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("FindBestStarOccultations() error = %v", err)
|
||
|
|
}
|
||
|
|
if len(results) != 1 {
|
||
|
|
t.Fatalf("FindBestStarOccultations() returned %d events, want 1", len(results))
|
||
|
|
}
|
||
|
|
result := results[0]
|
||
|
|
if result.Observer.Longitude < -180 || result.Observer.Longitude > 180 || result.Observer.Latitude < -90 || result.Observer.Latitude > 90 {
|
||
|
|
t.Fatalf("best observer out of bounds: %+v", result.Observer)
|
||
|
|
}
|
||
|
|
if result.Observer.Height != 0 {
|
||
|
|
t.Fatalf("best observer height = %.1f, want sea-level default", result.Observer.Height)
|
||
|
|
}
|
||
|
|
if result.MinimumSeparationArcsec > result.MoonSemidiameterArcsec {
|
||
|
|
t.Fatalf("best point is not an occultation: separation=%.3f radius=%.3f", result.MinimumSeparationArcsec, result.MoonSemidiameterArcsec)
|
||
|
|
}
|
||
|
|
if !result.VisibleAtGreatest || result.MoonAltitudeAtGreatest < 0 {
|
||
|
|
t.Fatalf("best observer must see the Moon: altitude=%.6f visible=%v", result.MoonAltitudeAtGreatest, result.VisibleAtGreatest)
|
||
|
|
}
|
||
|
|
if result.Immersion.IsZero() || result.Emersion.IsZero() || !result.Immersion.Before(result.Greatest) || !result.Greatest.Before(result.Emersion) {
|
||
|
|
t.Fatalf("best point contact ordering invalid: immersion=%v greatest=%v emersion=%v", result.Immersion, result.Greatest, result.Emersion)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestFindStarOccultationsLatitudePrefilterRejectsPolarStar(t *testing.T) {
|
||
|
|
star := StarCoordinate{
|
||
|
|
ID: "polar-star",
|
||
|
|
RA: 0,
|
||
|
|
Dec: 89,
|
||
|
|
Epoch: time.Date(2000, 1, 1, 12, 0, 0, 0, time.UTC),
|
||
|
|
Frame: CoordinateFrameICRS,
|
||
|
|
}
|
||
|
|
results, err := FindStarOccultations(
|
||
|
|
time.Date(2026, 8, 1, 0, 0, 0, 0, time.UTC),
|
||
|
|
time.Date(2026, 9, 1, 0, 0, 0, 0, time.UTC),
|
||
|
|
star, 0, 0, 0, OccultationSearchOptions{})
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("FindStarOccultations() error = %v", err)
|
||
|
|
}
|
||
|
|
if len(results) != 0 {
|
||
|
|
t.Fatalf("polar star returned %d events, want none", len(results))
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestFindStarOccultationsReturnsCompleteContactsOutsideQueryWindow(t *testing.T) {
|
||
|
|
observer := Observer{Longitude: 121.4737, Latitude: 31.2304, Height: 4}
|
||
|
|
eventTime := time.Date(2026, 8, 2, 12, 0, 0, 0, time.UTC)
|
||
|
|
ra, dec := occultationTestMoonTopocentricRaDec(occultationTestTimeToTT(eventTime), observer, -1)
|
||
|
|
star := StarCoordinate{ID: "clipped-window", RA: ra, Dec: dec, Epoch: eventTime, Frame: CoordinateFrameApparentOfDate}
|
||
|
|
start := eventTime.Add(-time.Minute)
|
||
|
|
end := eventTime.Add(time.Minute)
|
||
|
|
|
||
|
|
results, err := FindStarOccultations(start, end, star,
|
||
|
|
observer.Longitude, observer.Latitude, observer.Height, OccultationSearchOptions{})
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("FindStarOccultations() error = %v", err)
|
||
|
|
}
|
||
|
|
if len(results) != 1 {
|
||
|
|
t.Fatalf("FindStarOccultations() returned %d events, want 1", len(results))
|
||
|
|
}
|
||
|
|
result := results[0]
|
||
|
|
if !result.ContactsComplete || result.Immersion.IsZero() || result.Emersion.IsZero() {
|
||
|
|
t.Fatalf("incomplete contacts: %+v", result)
|
||
|
|
}
|
||
|
|
if !result.Immersion.Before(start) || !result.Emersion.After(end) {
|
||
|
|
t.Fatalf("contacts were clipped to query window: immersion=%v window=%v..%v emersion=%v", result.Immersion, start, end, result.Emersion)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func occultationTestTimeToTT(value time.Time) float64 {
|
||
|
|
return basic.TD2UT(basic.Date2JDE(value.UTC()), true)
|
||
|
|
}
|
||
|
|
|
||
|
|
func occultationTestMoonTopocentricRaDec(tt float64, observer Observer, n int) (float64, float64) {
|
||
|
|
ra, dec := basic.HMoonGeocentricApparentRaDecN(tt, n)
|
||
|
|
distanceAU := basic.HMoonAwayN(tt, n) / 149597870.7
|
||
|
|
ra, dec = basic.TopocentricRaDec(ra, dec, observer.Latitude, observer.Longitude, basic.TD2UT(tt, false), distanceAU, observer.Height)
|
||
|
|
ra = math.Mod(ra, 360)
|
||
|
|
if ra < 0 {
|
||
|
|
ra += 360
|
||
|
|
}
|
||
|
|
return ra, dec
|
||
|
|
}
|
||
|
|
|
||
|
|
func hr4799CoordinateForTest() StarCoordinate {
|
||
|
|
return StarCoordinate{
|
||
|
|
ID: "HR 4799",
|
||
|
|
RA: 189.1975,
|
||
|
|
Dec: -5.831944444444,
|
||
|
|
Epoch: time.Date(2000, 1, 1, 12, 0, 0, 0, time.UTC),
|
||
|
|
Frame: CoordinateFrameJ2000,
|
||
|
|
ProperMotionRACosDecMasPerYear: -28,
|
||
|
|
ProperMotionDecMasPerYear: -18,
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestFindStarOccultationsValidatesInputs(t *testing.T) {
|
||
|
|
star := StarCoordinate{
|
||
|
|
RA: 10,
|
||
|
|
Dec: 20,
|
||
|
|
Epoch: time.Date(2000, 1, 1, 12, 0, 0, 0, time.UTC),
|
||
|
|
Frame: CoordinateFrameICRS,
|
||
|
|
}
|
||
|
|
_, err := FindStarOccultations(time.Time{}, time.Time{}, star, 0, 0, 0, OccultationSearchOptions{})
|
||
|
|
if !errors.Is(err, ErrInvalidOccultationInput) {
|
||
|
|
t.Fatalf("FindStarOccultations() error = %v, want ErrInvalidOccultationInput", err)
|
||
|
|
}
|
||
|
|
start := time.Date(2026, 8, 2, 0, 0, 0, 0, time.UTC)
|
||
|
|
_, err = FindStarOccultations(start, start, star, 0, 0, 0, OccultationSearchOptions{})
|
||
|
|
if !errors.Is(err, ErrInvalidOccultationInput) {
|
||
|
|
t.Fatalf("FindStarOccultations() unordered range error = %v, want ErrInvalidOccultationInput", err)
|
||
|
|
}
|
||
|
|
_, err = FindStarOccultations(start, start.Add(time.Hour), star, math.NaN(), 0, 0, OccultationSearchOptions{})
|
||
|
|
if !errors.Is(err, ErrInvalidOccultationInput) {
|
||
|
|
t.Fatalf("FindStarOccultations() observer error = %v, want ErrInvalidOccultationInput", err)
|
||
|
|
}
|
||
|
|
}
|