feat: 扩展天文计算能力
- 新增日食、月食、本地可见性、中心线、半影区域、SVG 图示与沙罗周期信息 - 新增行星冲合、留、方照、物理星历、视直径、相位、亮肢角、轨道节点等计算 - 新增木星伽利略卫星位置、现象与接触事件计算 - 新增恒星星表、星座判定、自行修正与观测辅助能力 - 新增 coord、formula、orbit、sundial、lite/sun、lite/moon 等扩展包 - 完善农历年号、月相英文别名、视差角、大气质量、折射、日晷与双星计算 - 增加 NASA、JPL Horizons、IMCCE 等回归测试数据与基线测试 - 重构基础算法文件组织,补充大量公开 API 注释和语义回归测试 - 更新中文和英文 README,补充示例、精度说明、SVG 配图
This commit is contained in:
@@ -0,0 +1,145 @@
|
||||
package basic
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"math"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
const galileanEventToleranceSeconds = 480.0
|
||||
|
||||
type galileanEventBaselineRecord struct {
|
||||
Label string `json:"label"`
|
||||
Satellite int `json:"satellite"`
|
||||
Type string `json:"type"`
|
||||
StartUTC string `json:"start_utc"`
|
||||
StartDurationMinutes float64 `json:"start_duration_minutes"`
|
||||
EndUTC string `json:"end_utc"`
|
||||
EndDurationMinutes float64 `json:"end_duration_minutes"`
|
||||
}
|
||||
|
||||
func TestJupiterGalileanPhenomenonEventsAgainstIMCCEBaseline(t *testing.T) {
|
||||
records := loadGalileanEventBaseline(t)
|
||||
maxStartDiff := 0.0
|
||||
maxEndDiff := 0.0
|
||||
for _, record := range records {
|
||||
startUTC := mustParseRFC3339Nano(t, record.StartUTC)
|
||||
endUTC := mustParseRFC3339Nano(t, record.EndUTC)
|
||||
queryBefore := startUTC.Add(-12 * time.Hour)
|
||||
queryAfter := endUTC.Add(12 * time.Hour)
|
||||
queryMid := startUTC.Add(endUTC.Sub(startUTC) / 2)
|
||||
phenomenonType := parseBasicGalileanPhenomenonType(t, record.Type)
|
||||
|
||||
next := NextJupiterGalileanPhenomenonEvent(Date2JDE(queryBefore.UTC()), record.Satellite, phenomenonType)
|
||||
last := LastJupiterGalileanPhenomenonEvent(Date2JDE(queryAfter.UTC()), record.Satellite, phenomenonType)
|
||||
closest := ClosestJupiterGalileanPhenomenonEvent(Date2JDE(queryMid.UTC()), record.Satellite, phenomenonType)
|
||||
|
||||
assertGalileanEventMatchesBaseline(t, record.Label+" next", next, record, startUTC, endUTC, &maxStartDiff, &maxEndDiff)
|
||||
assertGalileanEventMatchesBaseline(t, record.Label+" last", last, record, startUTC, endUTC, &maxStartDiff, &maxEndDiff)
|
||||
assertGalileanEventMatchesBaseline(t, record.Label+" closest", closest, record, startUTC, endUTC, &maxStartDiff, &maxEndDiff)
|
||||
}
|
||||
t.Logf("galilean event baseline max diff: start=%.1fs end=%.1fs", maxStartDiff, maxEndDiff)
|
||||
}
|
||||
|
||||
func assertGalileanEventMatchesBaseline(
|
||||
t *testing.T,
|
||||
name string,
|
||||
event JupiterGalileanPhenomenonEvent,
|
||||
record galileanEventBaselineRecord,
|
||||
startUTC, endUTC time.Time,
|
||||
maxStartDiff, maxEndDiff *float64,
|
||||
) {
|
||||
t.Helper()
|
||||
if !event.Valid {
|
||||
t.Fatalf("%s invalid event", name)
|
||||
}
|
||||
if event.Satellite != record.Satellite {
|
||||
t.Fatalf("%s satellite mismatch: got %d want %d", name, event.Satellite, record.Satellite)
|
||||
}
|
||||
if string(event.Type) != record.Type {
|
||||
t.Fatalf("%s type mismatch: got %q want %q", name, event.Type, record.Type)
|
||||
}
|
||||
gotStart := JDE2DateByZone(event.Start, time.UTC, false)
|
||||
gotEnd := JDE2DateByZone(event.End, time.UTC, false)
|
||||
startDiff := math.Abs(gotStart.Sub(startUTC).Seconds())
|
||||
endDiff := math.Abs(gotEnd.Sub(endUTC).Seconds())
|
||||
if startDiff > *maxStartDiff {
|
||||
*maxStartDiff = startDiff
|
||||
}
|
||||
if endDiff > *maxEndDiff {
|
||||
*maxEndDiff = endDiff
|
||||
}
|
||||
if startDiff > galileanEventToleranceSeconds {
|
||||
t.Fatalf("%s start mismatch: got %s want %s", name, gotStart.Format(time.RFC3339Nano), startUTC.Format(time.RFC3339Nano))
|
||||
}
|
||||
if endDiff > galileanEventToleranceSeconds {
|
||||
t.Fatalf("%s end mismatch: got %s want %s", name, gotEnd.Format(time.RFC3339Nano), endUTC.Format(time.RFC3339Nano))
|
||||
}
|
||||
if !(event.Start <= event.Greatest && event.Greatest <= event.End) {
|
||||
t.Fatalf("%s greatest not inside event: start=%.9f greatest=%.9f end=%.9f", name, event.Start, event.Greatest, event.End)
|
||||
}
|
||||
if !galileanPhenomenonFlag(event.GreatestPhenomenon, event.Type) {
|
||||
t.Fatalf("%s greatest state is not active", name)
|
||||
}
|
||||
if jupiterGalileanPhenomenonMetricAt(event.Start-5.0/86400.0, event.Satellite, event.Type).active {
|
||||
t.Fatalf("%s still active 5s before start", name)
|
||||
}
|
||||
if jupiterGalileanPhenomenonMetricAt(event.End+5.0/86400.0, event.Satellite, event.Type).active {
|
||||
t.Fatalf("%s still active 5s after end", name)
|
||||
}
|
||||
}
|
||||
|
||||
func galileanPhenomenonFlag(phenomenon JupiterGalileanPhenomenon, phenomenonType JupiterGalileanPhenomenonType) bool {
|
||||
switch phenomenonType {
|
||||
case JupiterGalileanTransit:
|
||||
return phenomenon.Transit
|
||||
case JupiterGalileanOccultation:
|
||||
return phenomenon.Occultation
|
||||
case JupiterGalileanEclipse:
|
||||
return phenomenon.Eclipse
|
||||
case JupiterGalileanShadowTransit:
|
||||
return phenomenon.ShadowTransit
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func parseBasicGalileanPhenomenonType(t *testing.T, value string) JupiterGalileanPhenomenonType {
|
||||
t.Helper()
|
||||
switch JupiterGalileanPhenomenonType(value) {
|
||||
case JupiterGalileanTransit, JupiterGalileanOccultation, JupiterGalileanEclipse, JupiterGalileanShadowTransit:
|
||||
return JupiterGalileanPhenomenonType(value)
|
||||
default:
|
||||
t.Fatalf("unknown galilean phenomenon type %q", value)
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func loadGalileanEventBaseline(t *testing.T) []galileanEventBaselineRecord {
|
||||
t.Helper()
|
||||
path := filepath.Join("..", "jupiter", "testdata", "galilean_events_imcce_2026.json")
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var records []galileanEventBaselineRecord
|
||||
if err := json.Unmarshal(data, &records); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(records) == 0 {
|
||||
t.Fatal("empty galilean event baseline")
|
||||
}
|
||||
return records
|
||||
}
|
||||
|
||||
func mustParseRFC3339Nano(t *testing.T, value string) time.Time {
|
||||
t.Helper()
|
||||
date, err := time.Parse(time.RFC3339Nano, value)
|
||||
if err != nil {
|
||||
t.Fatalf("parse %q: %v", value, err)
|
||||
}
|
||||
return date
|
||||
}
|
||||
Reference in New Issue
Block a user