9ee2163cc7
- 新增月掩恒星和行星:支持搜索、掩甚点、全球掩带及固定地点轨迹计算 - 支持恒星星表坐标转换、有限盘面行星接触事件和月掩 SVG 输出 - 新增日月食及月掩全球投影图、时间标记和 GeoJSON 地理数据接口 - 扩展日食中心线、南北界及偏食足迹采样,支持极区投影 - 修正站心时角、月出月落、月球视半径、折射和恒星自行计算 - 优化内外行星事件搜索、边界选择、极端输入处理和计算稳定性
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)
|
|
}
|
|
}
|