Files
astro/jupiter/phenomena_contact_events_test.go
T
b612 16c62a97d5 feat: 完善时标与天象几何计算并扩展输出接口
- 新增时标、ΔT 模型、质心时间与 UT1 支持
- 改进日月食、月掩、行星事件及路径边界计算
- 完善恒星三维自行与动态距离传播
- 扩展 SVG、GeoJSON、KML 输出与底层距离换算工具
- 整理中英文手册、示例资源及回归测试
2026-09-23 18:55:12 +08:00

163 lines
5.3 KiB
Go

package jupiter
import (
"testing"
"time"
"b612.me/astro/basic"
)
func TestGalileanPhenomenonContactEventWrappersMatchBasic(t *testing.T) {
records := loadGalileanPublicEventBaseline(t)
loc := time.FixedZone("UTC+8", 8*3600)
for _, record := range records {
startUTC := mustParseGalileanEventTime(t, record.StartUTC)
endUTC := mustParseGalileanEventTime(t, record.EndUTC)
queryMid := startUTC.Add(endUTC.Sub(startUTC) / 2).In(loc)
phenomenonType := GalileanPhenomenonType(record.Type)
got := ClosestGalileanPhenomenonContactEvent(queryMid, record.Satellite, phenomenonType)
want := basic.ClosestJupiterGalileanPhenomenonContactEvent(
basic.Date2JD(queryMid.UTC()),
record.Satellite,
basic.JupiterGalileanPhenomenonType(phenomenonType),
)
assertGalileanContactWrapperMatchesBasic(t, record.Label, got, want, loc)
}
}
func assertGalileanContactWrapperMatchesBasic(
t *testing.T,
name string,
got GalileanPhenomenonContactEvent,
want basic.JupiterGalileanPhenomenonContactEvent,
loc *time.Location,
) {
t.Helper()
if got.Valid != want.Valid {
t.Fatalf("%s valid mismatch: got %v want %v", name, got.Valid, want.Valid)
}
if !got.Valid {
return
}
if got.Greatest.Location() != loc {
t.Fatalf("%s greatest timezone mismatch", name)
}
wantGreatest := basic.JD2DateByZone(want.Greatest, loc, false)
if !got.Greatest.Equal(wantGreatest) {
t.Fatalf("%s greatest mismatch: got %s want %s", name, got.Greatest.Format(time.RFC3339Nano), wantGreatest.Format(time.RFC3339Nano))
}
if got.Satellite != want.Satellite || string(got.Type) != string(want.Type) {
t.Fatalf("%s id/type mismatch", name)
}
assertGalileanContactMatchesBasic(t, name+" disappearance", got.Disappearance, want.Disappearance, loc)
assertGalileanContactMatchesBasic(t, name+" reappearance", got.Reappearance, want.Reappearance, loc)
assertSameGalileanPhenomenon(t, name+" greatest", got.GreatestState, want.GreatestPhenomenon)
}
func assertGalileanContactMatchesBasic(
t *testing.T,
name string,
got GalileanPhenomenonContact,
want basic.JupiterGalileanPhenomenonContact,
loc *time.Location,
) {
t.Helper()
if got.Valid != want.Valid {
t.Fatalf("%s valid mismatch", name)
}
if !got.Valid {
return
}
wantStart := basic.JD2DateByZone(want.Start, loc, false)
wantModel := basic.JD2DateByZone(want.ModelCrossing, loc, false)
wantEnd := basic.JD2DateByZone(want.End, loc, false)
if got.Start.Location() != loc || got.ModelCrossing.Location() != loc || got.End.Location() != loc {
t.Fatalf("%s timezone mismatch", name)
}
if !got.Start.Equal(wantStart) || !got.ModelCrossing.Equal(wantModel) || !got.End.Equal(wantEnd) {
t.Fatalf(
"%s time mismatch: got [%s %s %s] want [%s %s %s]",
name,
got.Start.Format(time.RFC3339Nano),
got.ModelCrossing.Format(time.RFC3339Nano),
got.End.Format(time.RFC3339Nano),
wantStart.Format(time.RFC3339Nano),
wantModel.Format(time.RFC3339Nano),
wantEnd.Format(time.RFC3339Nano),
)
}
if got.Duration != got.End.Sub(got.Start) {
t.Fatalf("%s duration mismatch", name)
}
if string(got.Phase) != string(want.Phase) {
t.Fatalf("%s phase mismatch: got %q want %q", name, got.Phase, want.Phase)
}
}
// IMCCE 年表每个事件给 4 个时刻:D1、D2 = D1+时长、F1、F2 = F1+时长。
func TestGalileanPhenomenonContactEventMatchesIMCCEBaseline(t *testing.T) {
records := loadGalileanPublicEventBaseline(t)
const (
contactTolerance = 120 * time.Second
durationTolerance = 25 * time.Second
)
var maxContact, maxDuration time.Duration
for _, record := range records {
startUTC := mustParseGalileanEventTime(t, record.StartUTC)
endUTC := mustParseGalileanEventTime(t, record.EndUTC)
startDuration := time.Duration(record.StartDurationMinutes * float64(time.Minute))
endDuration := time.Duration(record.EndDurationMinutes * float64(time.Minute))
endOfEvent := endUTC.Add(endDuration)
midpoint := startUTC.Add(endOfEvent.Sub(startUTC) / 2)
got := ClosestGalileanPhenomenonContactEvent(midpoint, record.Satellite, GalileanPhenomenonType(record.Type))
if !got.Valid {
t.Fatalf("%s returned no contact event", record.Label)
}
contacts := []struct {
name string
got time.Time
want time.Time
}{
{"D1", got.Disappearance.Start, startUTC},
{"D2", got.Disappearance.End, startUTC.Add(startDuration)},
{"F1", got.Reappearance.Start, endUTC},
{"F2", got.Reappearance.End, endOfEvent},
}
for _, contact := range contacts {
delta := contact.got.Sub(contact.want)
if delta < 0 {
delta = -delta
}
if delta > maxContact {
maxContact = delta
}
if delta > contactTolerance {
t.Fatalf("%s %s offset %v exceeds %v", record.Label, contact.name, contact.got.Sub(contact.want), contactTolerance)
}
}
durations := []struct {
name string
got time.Duration
want time.Duration
}{
{"D", got.Disappearance.Duration, startDuration},
{"F", got.Reappearance.Duration, endDuration},
}
for _, duration := range durations {
delta := duration.got - duration.want
if delta < 0 {
delta = -delta
}
if delta > maxDuration {
maxDuration = delta
}
if delta > durationTolerance {
t.Fatalf("%s %s-phase duration offset %v exceeds %v", record.Label, duration.name, duration.got-duration.want, durationTolerance)
}
}
}
t.Logf("contact offset max %v, duration offset max %v", maxContact, maxDuration)
}